BOOK 6 - 이펙티브 자바(2)

2장 객체 생성과 파괴

생성자 대신 정적 팩터리 메서드를 고려하라

  • 클래스는 생성자와 별도로 static factory method를 제공할 수 있다.
public static Boolean valueOf(boolean b) {
  return b ? Boolean.TRUE: Boolean.FALSE;
}

static factory method의 장점과 단점

장점

  • 이름을 가질 수 있다.
    • 메서드 이름을 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.
    • 생성자는 매개변수와 생성자 자체만으로 반환될 객체의 특성을 설명하기 어렵다.
  • 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
    • 인스턴스를 미리 만들어 놓거나, 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.
    • 생성 비용이 큰 같은 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올려준다.
      • 플라이웨이트 패턴(Flyweight pattern)
    • 정적 팩터리 메서드 방식의 클래스는 언제 어느 인스턴스를 살아있게 할지 통제할 수 있는 instance-controlled 클래스가 된다.
      • 인스턴스를 통제하면 클래스를 Singleton으로 만들 수도 있고, noninstantiable(인스턴스화 불가) 하게 만들수도 있다.
  • 반환 타입의 하위 타입 객체를 반환할 수 있다.
    • 반환할 객체 타입을 자유롭게 선택할 수 있는 엄청난 유연성이 생긴다.
    • API를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.
  • 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
    • 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다.
    • 다음 릴리즈에서 성능을 개성한 또 다른 클래스의 객체를 반환해도 된다.
    • 클라이언트는 팩터리가 반환하는 객체가 어느 클래스의 인스턴스인지 알 필요가 없다.
  • 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
    • 이런 유연함은 service provider framework를 만드는 근간이 된다.

단점

  • 상속을 하기 위해서는 public / protected 생성자가 필요하기 때문에 static factory method만 제공하면 하위 클래스를 만들 수 없다.

  • 개발자가 정적 팩터리 메서드를 찾기 어렵다.

    • 생성자처럼 API 설명에 명확하게 드러나지 않아 사용자가 클래스를 인스턴스화할 방법을 알아내야 한다.
  • 정적 팩터리 메서드에서 흔히 사용하는 명명 방식은 다음이 있다.

    • from : 매개변수 하나를 받아서 해당 타입 인스턴스 반환하는 형변환 메서드

    • of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드

    • valueOf

    • instance / getInstance : 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.

    • create / newInstance : instance / getInstance 와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.

    • getType : getInstance 와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다.

      FileStore fs = Files.getFileStore(path);
      
    • newType : newInstance 와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다.

      BufferedReader br = Files.newBufferedReader(path);
      
    • type : getType / newType 의 간결한 버전

      List<Complaint> litany = Collections.list(legacyLitany);
      

생성자에 매개변수가 많다면 빌더를 고려하라

  • 생성자나 정적 팩터리 메서드는 하나의 제약이 있다.
    • 선택적 매개변수가 많을 때 적절히 대응하기 어렵다.
    • 생성자는 사용자가 설정하길 원치 않는 매개변수까지 포함하고 있는 경우가 많아 어쩔 수 없이 그런 매개변수까지 값을 지정해줘야 한다.

고전적 해결방법

  • 점층적 생성자 패턴
    • 필수 매개변수만 받는 생성자, 필수 매개변수에 선택 매개변수 1개, 2개, 3개, … 받는 생성자 형태로 생성자를 늘려가는 방식
    • 코드를 읽을 때 각 값의 의미가 무엇인지 파악하기 어렵고 버그로 이어질 수 있다.
  • 자바빈즈 패턴
    • 매개변수 없는 생성자로 객체를 만든 후, setter 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식
    • 점층적 생성자 패턴에 비해서 코드가 길어졌지만 인스턴스를 만들기 쉽고, 읽기 쉬운 코드가 된다.
    • 객체 하나를 만드려면 메서드 여러개를 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태에 놓이게 된다.
      • 클래스를 불변으로 만들 수 없고, 스레드 안정성을 얻으려면 추가 작업을 해줘야 한다.

빌더 패턴 (Builder pattern)

  • 클라이언트는 객체를 직접 만드는 대신, 필수 매개변수 만으로 생성자를 호출해 빌더 객체를 얻는다.
  • 빌더 객체가 제공하는 일종의 setter 메서드들로 원하는 선택 매개변수들을 설정한다.
  • 마지막으로 build 메서드를 호출해 필요한 객체를 얻는다.
public class NutritionFacts {
  private final int servingSize;
  private final int servings;
  private final int calories;
  private final int fat;
  private final int sodium;
  private final int carbohydrate;
  
  public static class Builder {
    // 필수 매개변수
    private final int servingSize;
    private final int servings;
    
    // 선택 매개변수 - 기본값으로 초기화한다.
    private int calories = 0;
    private int fat = 0;
    private int sodium = 0;
    private int carbohydrate = 0;
    
    public Builder(int servingSize, int servings) {
      this.servingSize = servingSize;
      this.servings = servings;
    }
    
    public Builder calories(int val) {
      calories = val;
      return this;
    }
    
    public Builder fat(int val) {
      fat = val;
      return this;
    }
    
    public Builder sodium(int val) {
      sodium = val;
      return this;
    }
    
    public Builder carbohydrate(int val) {
      carbohydrate = val;
      return this;
    }
    
    public NutritionFacts build() {
      return new NutritionFacts(this);
    }
  }
  
  private NutritionFacts(Builder builder) {
    servingSize = builder.servingSize;
    servings = builder.servings;
    calories = builder.calories;
    fat = builder.fat;
    sodium = builder.sodium;
    carbohydrate = builder.carbohydrate;
  }
}
  • NutritionFacts 클래스는 불변이며, 모든 매개변수의 기본값들을 한 곳에 모아두었다.
  • 빌더 패턴을 이용한 코드는 빌더의 세터 메서드들이 빌더 자신을 반환하기 때문에 method chaining으로 호출된다.
    • 클라이언트 코드는 읽기 쉬워진다.

계층적 클래스의 빌더 패턴

  • 추상 클래스는 추상 빌더를, 구현 클래스는 구체 빌더를 갖게 한다.
public abstract class Pizza {
  public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
  final Set<Topping> toppings;
  
  abstract static class Builder<T extends Builder<T>> {
    EnumSet<Topping> toppings  = EnumSet.noneOf(Topping.class);
    
    public T addTopping(Topping topping) {
      toppings.add(Objects.requireNonNull(topping));
      return self();
    }
    
    abstract Pizza build();
    
    // 하위 클래스에서 override해서 this를 반환하도록 한다.
    protected abstract T self();
  }
  
  Pizza(Builder<?> builder) {
    toppings = builder.toppings.clone();
  }
}
  • Pizza.Builder 클래스는 재귀적 타입 한정을 이용하는 제네릭 타입이다.
  • 추상 클래스인 self() 를 사용해 하위 클래스에서 형변환하지 않고도 method chaining을 지원할 수 있다.
  • Pizza 의 하위 클래스인 NyPizza 클래스를 보자.
public class NyPizza extends Pizza {
  public enum Size { SMALL, MEDIUM, LARGE }
  private fianl Size size;
  
  public static class Builder extends Pizza.Builder<Builder> {
    private final Size size;
    
    public Builder(Size size) {
      this.size = Objects.requireNonNull(size);
    }
    
    @Override
    public NyPizza build() {
      return new NyPizza(this);
    }
    
    @Override
    protected Builder self() {
      return this;
    }
  }
  
  private NyPizza(Builder builder) {
    super(builder);
    size = builder.size;
  }
}

private 생성자나 열거 타입으로 싱글턴임을 보증하라

  • 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기 어려워질 수 있다.
    • 싱글턴 인스턴스를 mock 구현으로 대체할 수 없기 때문이다.
  • 싱글턴을 만드는 방식은 보통 두가지 중 하나다.

public static final 필드 방식

public class Elvis {
  public static final Elvis INSTANCE = new Elvis();
  private Elvis() { ... }
}
  • private 생성자는 Elvis.INSTANCE 를 초기화할 때 딱 한번 호출된다.
  • public / protected 생성자가 없으므로 Elvis 클래스의 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.

정적 팩터리 방식

public class Elvis {
  private static final Elvis INSTANCE = new Elvis();
  private Elvis() { ... }
  
  public static Elvis getInstance() {
    return INSTANCE;
  }
}
  • 이 방식의 장점은 API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다는 점이다.
  • 두번째 장점은 원한다면 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다는 점이다.
  • 세번째는 정적 팩터리의 메서드 참조를 supplier로 사용할 수 있다는 점이다.
    • Elvis::getInstanceSupplier<Elvis> 로 사용하는 식이다.
싱글턴 클래스의 직렬화
  • 싱글턴 클래스를 직렬화하려면 단순히 Serializable 을 구현한다고 선언하는 것만으로는 부족하다.

  • 모든 인스턴스 필드를 transient 라고 선언하고, readResolve 메서드를 제공해야 한다.

    • 이렇게 하지 않으면 직렬화된 인스턴스를 역직렬화할 때 새로운 인스턴스가 만들어진다.
    private Object readResolve() {
      return INSTANCE;
    }
    
  • 이 외에 싱글턴을 만드는 세번째 방법은 원소가 하나인 열거 타입을 선언하는 방식이다.

열거 타입 방식

public enum Elvis {
  INSTANCE;
  
  ...
}
  • public 필드 방식과 비슷하지만, 더 간결하고, 추가 노력없이 직렬화할 수 있다.

  • 부자연스러워 보일 수 있으나, 대부분 상황에서 원소가 하나뿐인 enum 타입이 싱글턴을 만드는 가장 좋은 방법이다.

인스턴스화를 막으려거든 private 생성자를 사용하라

  • 정적 멤버만 담은 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한 것이 아니다.
  • 하지만 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들어준다.
  • 인스턴스화를 막기 위해서는 컴파일러가 기본 생성자를 만들지 않도록 해야 한다.
    • 컴파일러가 기본 생성자를 만드는 경우는 명시된 생성자가 없을 때이므로 private 생성자를 추가하면 된다.
public class UtilityClass {
  private UtilityClass() {
  	throw new AssertionError();
	}
  ...
}
  • 생성자를 private으로 선언했으므로, 해당 클래스를 상속할 수 없게 하는 효과도 있다.
    • 모든 생성자는 명시적이든 묵시적이든 상위 클래스의 생성자를 호출하기 때문이다.

자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

  • 많은 클래스가 하나 이상의 자원에 의존한다.
  • 이런 클래스들을 정적 유틸리티 클래스로 구현한 경우가 드물지 않다.
public class SpellChecker {
  private static final Lexicon dictionary = ...;
  
  private SpellChecker() {}
  
  public static boolean isValid(String word) { ... }
  public static List<String> suggestions(String typo) { ... }
}
public class SpellChecker {
	private static final Lexicon dictionary = ...;
  
  private SpellChecker() {}
  public static SpellChecker INSTANCE = new SpellChecker();
  
  public static boolean isValid(String word) { ... }
  public static List<String> suggestions(String typo) { ... }
}
  • 위는 인스턴스 생성을 방지한 유틸리티 클래스로 구현한 방식이고, 아래는 싱글턴으로 구현한 방식이다.
  • 두 방식 모두 Lexicon 을 단 하나만 사용한다고 가정한다는 점에서 좋아보이지 않는다.
    • 실전에서는 사전이 언어별로 따로 있고, 특수 어휘용 사전, 테스트용 사전 등등이 있을 수 있다.
  • 사용하는 자원에 따라 동작이 달라지는 클래스는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.
    • 클래스는 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원을 사용해야 한다.
    • 이 조건을 만족하기 위해 인스턴스를 생성할 때 필요한 자원을 넘겨주는 방식 을 사용한다.
    • 이는 의존 객체 주입의 한 형태이다.
public class SpellChecker {
	private final Lexicon dictionary;
  
  public SpellChecker(Lexicon dictionary) {
    this. dictionary = Objects.requireNonNull(dictionary);
  }
  
  public static boolean isValid(String word) { ... }
  public static List<String> suggestions(String typo) { ... }
}
  • 의존 객체 주입 패턴은 자원이 몇개든 의존 관계가 어떻든 상관없이 잘 동작한다.
  • 불변을 보장하여 같은 자원을 사용하려는 여러 클라이언트가 의존 객체들을 안심하고 공유할 수 있기도 하다.

  • 이 패턴의 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 있다.

    • factory란, 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체이다.
    • Supplier<T> 인터페이스가 팩터리를 표현한 예다.
      • Supplier<T> 를 입력으로 받는 메서드는 일반적으로 한정적 wildcard 타입을 사용해 팩터리의 타입 매개변수를 제한한다.
    • 클라이언트는 자신이 명시한 타입의 하위 타입이라면 무엇이든 생성할 수 있는 factory를 넘길 수 있다.
  • 아래 코드는 클라이언트가 제공한 팩터리가 생성한 타일(Tile)들로 구성된 모자이크(Mosaic)를 만드는 메서드다.
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

불필요한 객체 생성을 피하라

  • 똑같은 기능의 객체를 매번 생성하기 보다는 객체 하나를 재사용하는 편이 나을 때가 많다.
  • 생성 비용이 비싼 객체들은 반복해서 필요하다면 캐싱하여 재사용하는 것이 좋다.
static boolean isRomanNumeral(String s) {
  return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
  • 위 코드에서 String.matches 메서드는 내부적으로 정규표현식용 Pattern 인스턴스를 만드는데, 이 인스턴스는 한번 쓰고 버려져 GC 대상이 된다.
    • Pattern 은 입력받은 정규표현식에 해당하는 유한 상태 머신을 만들기 때문에 인스턴스 생성 비용이 높다.
    • 성능이 중요한 상황에서 반복적으로 사용하기 적절하지 않다.
  • 성능을 개선하기 위해서 Pattern 인스턴스를 클래스 초기화 과정에 직접 생성하여 캐싱해두고 재사용하도록 한다.
public class RomanNumerals {
  private static final Pattern ROMAN = Pattern.compile(
    "^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
  
  static boolean isRomanNumeral(String s) {
    return ROMAN.matcher(s).matches();
  }
}
  • auto boxing도 불필요한 객체를 만들어내는 또 다른 예이다.
    • auto boxing은 개발자가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다.
private static long sum() {
  Long sum = 0L;
  for(long i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i;
  }
  return sum;
}
  • 위 코드는 정확히 동작하기는 하지만 성능상 매우 안 좋고, 느리다.
  • sum 변수를 Long 타입으로 선언해 불필요한 Long 인스턴스가 약 2^31개나 만들어지기 때문이다.
    • long 타입인 i 변수가 sum 변수에 더해질 때마다 새로운 Long 인스턴스가 생성된다.
  • 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 auto boxing이 발생하지 않도록 주의하는 것이 좋다.

  • 객체 생성은 무조건 비싸니 피해야하는 것은 아니다.
  • 오히려 아주 무거운 객체가 아닌 이상 단순히 객체 생성을 피하고자 객체 pool을 만들지 말자.
    • 데이터베이스 연결 같은 경우는 객체 생성 비용이 비싸기 때문에 재사용하는 것이 좋지만, 일반적으로는 자체 객체 풀은 코드를 복잡하게 만들고, 메모리 사용량을 늘리고, 성능을 떨어뜨린다.

다 쓴 객체 참조를 해제하라

  • 자바는 가비지 컬렉터가 알아서 다 쓴 객체를 회수해가기 때문에 메모리 관리를 하지 않아도 된다고 생각할 수 있지만, 그렇지 않다.
public class Stack {
  private Object[] elements;
  private int size = 0;
  private static final int DEFAULT_INITIAL_CAPACITY = 16;
  
  public Stack() {
    elements = new Object[DEFAULT_INITIAL_CAPACITY];
  }
  
  public void push(Object e) {
    ensureCapacity();
    elements[size++] = e;
  }
  
  public Object pop() {
    if(size = 0) {
      throw new EmptyStackException();
    }
    return elements[--size];
  }
  
  private void ensureCapacity() {
    if(elements.length == size) {
      elements = Arrays.copyOf(elements, 2 * size + 1);
    }
  }
}
  • 위 코드는 특별한 문제 없이 동작하지만, 이 스택을 사용하는 프로그램을 오래 실행하다보면 점차 메모리 사용량이 늘어나 성능이 저하될 것이다.
  • 위 코드에서는 스택에서 아이템을 pop할 때 꺼내진 객체들이 더이상 사용되지 않더라도 GC가 회수해가지 않는다.
    • 이 스택이 그 객체들을 여전히 참조하고 있기 때문이다.
  • 객체 참조 하나를 살려두면, GC는 그 객체뿐만 아니라 그 객체가 참조하는 모든 객체, 그리고 또 그 객체가 참조하는 모든 객체, …를 전부 회수해가지 못한다.
    • 단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고, 성능에 악영향을 줄 수 있다.
  • 이를 해결하기 위해서는 다 쓴 참조는 null 처리하면 된다.
public Object pop() {
  if (size == 0) {
    throw new EmptyStackException();
  }
  
  Object result = elements[--size];
  elements[size] = null;
  return result;
} 
  • Stack 클래스처럼 자기 메모리를 직접 관리하는 클래스는 항상 메모리 누수에 주의해야 한다.

메모리 누수의 다른 원인들

캐시

  • 객체 참조를 캐시에 넣어놓고, 객체를 다 쓴 뒤로도 한참을 그냥 놔두는 경우가 많다.
  • 보통 캐시 entry의 유효 기간을 정확히 정의하기 어렵기 때문에, 시간이 지날수록 entry의 가치를 떨어뜨리는 방식을 흔히 사용한다.
    • ScheduledThreadPoolExecutor 같은 백그라운드 스레드를 활용하거나 캐시에 새로운 entry를 추가할 때 부수작업으로 쓰지않는 entry를 청소해줘야 한다.

listener와 callback

  • 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면 콜백은 계속 쌓여갈 것이다.
  • 콜백을 약한 참조(weak reference)로 저장하면 GC가 즉시 수거해간다.

finalizer와 cleaner 사용을 피하라

  • 자바는 두가지의 객체 소멸자를 제공한다.
  • 하지만 이 두가지는 예측할 수 없고, 상황에 따라 위험할 수 있고, 불필요하다.

  • finalizer와 cleaner는 즉시 수행된다는 보장이 없다.
    • finalizer와 cleaner를 얼마나 신속히 수행할지는 전적으로 가비지 컬렉터 알고리즘에 달렸으며, 구현마다 천차만별이다.
    • 예를 들어 파일 닫기를 finalizer나 cleaner에게 맡기면 언제 실행될지 알 수 없어 시스템이 동시에 열 수 있는 파일 개수를 초과해 문제를 일으킬 수 있다.
  • finalizer와 cleaner는 심각한 성능 문제도 동반한다.

finalizer나 cleaner를 대신할 방법

  • 파일이나 스레드 등 종료해야할 자원을 담고 있는 객체의 클래스에서 finalizer나 cleaner를 대신할 방법은 무엇일까
  • AutoCloseable 을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 된다.
    • 일반적으로 예외가 발생해도 제대로 종료되도록 try-with-resources 를 사용해야 한다.

try-finally 보다는 try-with-resources 를 사용하라

  • 자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다.
    • InputStream, OutputStream , java.sql.Connection 등이 그 예이다.
  • 이런 자원들 중 상당수가 안전망으로 finalizer를 활용하고 있지만, finalizer는 그리 믿을만하지 못하다.

try-finally

  • 전통적으로 자원을 닫는 수단으로 try-finally 를 사용했다.
  • 하지만 이 방식은 자원이 둘 이상인 경우에 코드가 지저분해진다.
static void copy(String src, String dst) throws IOException {
  InputStream in = new FileInputStream(src);
  try {
    OutputStream out = new FileOutputStream(dst);
    try {
      byte[] buf = new byte[BUFFER_SIZE];
      int n;
      while((n = in.read(buf)) >= 0)
        out.write(buf, 0, n);
    } finally {
      out.close();
    }
  } finally {
    in.close();
  }
}

try-with-resources

  • 이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 한다.
    • void 를 반환하는 close 메서드를 정의한 인터페이스이다.
    • 자바 라이브러리와 3rd party 라이브러리들의 수많은 클래스와 인터페이스가 이미 AutoCloseable 을 구현해뒀다.
static String firstLineOfFile(String path) throws IOException {
  try (BufferedReader br = new BufferedReader(new FileReader(path))) {
    return br.readLine();
  }
}
static void copy(String src, String dst) throws IOException {
  try (InputStream in = new FileInputStream(src);
      OutputStream out = new FileOutputStream(dst)) {
    byte[] buf = new byte[BUFFER_SIZE];
    int n;
    while((n = in.read(buf)) >= 0) 
      out.write(buf, 0, n);
  }
}
  • 기기에 문제가 생겨 firstLineOfFile 메서드의 readLine 에서 예외가 발생하고, 같은 이유로 close 에서도 예외가 발생하는 경우를 생각해보자.
    • close 에서 발생한 예외는 숨겨지고, readLine 에서 발생한 예외가 기록될 것이다.
    • 숨겨진 예외들도 버려지지는 않고 stack trace에 “suppressed” 라는 꼬리표를 달고 출력된다.
    • 앞의 try-finally 에서는 close 에서 발생한 예외가 처음 발생한 readLine 의 예외를 집어 삼켜버려 첫번째 예외에 관한 정보는 남지 않게 되는 문제가 있다.
      • 이는 처음 발생한 예외를 숨겨버려 디버깅을 어렵게 한다.

catch

  • try-finally 에서처럼 try-with-resources 에서도 catch 절을 쓸 수 있다.
static String firstLineOfFile(String path, String defaultVal) {
  try (BufferedReader br = new BufferedReader(new FileReader(path))) {
    return br.readLine();
  } catch (IOException e) {
    return defaultVal;
  }
}