Java 7의 변화는 변경된 부분과 추가된 부분으로 나누어 볼 수 있다. 먼저 변경된 부분은 다음과 같다.
숫자를 표현할 때 아무런 접두사를 숫자 앞에 넣지 않으면 10진수로 인식한다. 그리고, 0을 숫자 앞에 넣어주면 8진수로, 0x를 숫자 앞에 넣어주면 16진수로 인식한다.
또한 Java 7부터는 2진수 표현이 추가되었다. 2진수로 나타내기 위해서는 0b를 앞에 추가해주면 된다.
또한 우리나라에서 돈 단위나 숫자 단위를 표시할 때는 1000 단위에 쉼표를 붙힌다. 예를 들어 백만의 경우에는 1,000,000과 같이 표시를 한다. 하지만, 이제는 _를 사용할 수 있다. 보통 2진수는 4자리 단위로 잘라서 표시한다. 그런데 _를 사용할 때는 한가지 유의해야 하는 것이 있다. 바로 _의 위치다. _를 사용할 때는 반드시 "숫자 사이에만" 넣어주어야 한다.
Java 6까지는 switch-case 문장에 정수형만 사용할 수 있었다. 그런데, Java 7부터는 switch 문장에 String도 사용할 수 있다.
지금까지는 숫자로만 switch-case문을 사용할 수 있었다. 보통은 숫자를 지정하는 것보다는 상수를 선언하여 사용한다. 혹은 enum을 사용해도 된다.
그리고, 또 한가지 유의할 점이 있다. 만약 switch-case에 사용하는 String 문자열이 null인 경우에는 NullPointerException이 발생하니 switch-case에서 사용하는 문자열에 null인지를 꼭 확인해야 한다.
제네릭을 사용할 때는 반드시 생성자에도 해당 타입들을 상세하게 명시했어야 했다. 하지만 Java 7 부터는 생성자에는 일일이 타입들을 명시해 줄 필요가 없다. 왜냐하면, 이미 변수를 선언할 때 필요한 타입들을 지정해 놓았기 때문이다. 그러므로, 다음과 같이 명시해주면 된다.
HashMap<String, Integer> map = new HashMap<>(),
Map<String, List<String>> map2 = new HashMap<>();
ArrayList<String> list = new ArrayList<>();
이름 그대로 <>로 표시한 것을 다이아몬드라고 명명햇으며, 이렇게 다이아몬드를 사용하면 컴파일러에서 알아서 해당 생성자의 타입을 지정해버린다.
이처럼 편리한 다이아몬드는 편한 만큼 제약이 따르며, 다음과 같은 제약들이 있다.
다음과 같은 제네릭 클래스가 있다.
package f.generic;
public class GenericClass <X>{
private X x;
private Object o;
public <T> GenericClass (T t){
this.o = t;
System.out.println("T type="+t.getClass().getName());
}
public void setValue(X x){
this.x = x;
System.out.println("X type="+x.getClass().getName());
}
}
X와 T라는 제네릭한 타입이 선언되어 있는 클래스다. 이 클래스의 객체를 생성하는 다음의 클래스를 살펴보자.
package f.generic;
public class TypeInference {
public static void main(String[] args){
TypeInference type = new TypeInference();
type.makeObject1();
}
public void makeObject1(){
GenericClass<Integer> generic1 = new GenericClass<>("String");
generic1.setValue(999);
}
}
이 클래스를 실행하고 컴파일하면 다음과 같은 결과가 나온다.
T type=java.lang.String
X type=java.lang.Integer
T의 타입은 String이고, X의 타입은 Integer다. 선언한 대로 타입들이 지정된 것을 볼 수 있다.
그러면 makeObject2() 메소드를 다음과 같이 만들어보자. 이번에는 생성자에 다이아몬드가 없다.
public void makeObject2(){
GenericClass<Integer> generic1 = new GenericClass("String");
generic1.setValue(999);
}
경고가 발생하지만, 실행하는 데는 큰 문제가 없다.
그렇다면 이번에는 다이아몬드를 안쓰고 명시적으로 지정할 때를 살펴보자.
public void makeObject3(){
GenericClass<Integer> generic1 = new <String> GenericClass<Integer>("String");
generic1.setValue(999);
}
명시적으로 이렇게 지정하면 큰 문제가 없을 것이다. 당연히 컴파일 시에는 오류도 없고, 실행도 정상적으로 된다.
그런데, 마지막으로 다음과 같이 X 타입을 다이아몬드로 선언해보자.
public void makeObject4(){
GenericClass<Integer> generic1 = new <String> GenericClass<>("String");
generic1.setValue();
}
그냥 보기에는 아무런 이상 없이 컴파일이 될 것 같지만, 실제로는 컴파일러가 혼동을 일으키게 된다.
java: cannot infer type arguments for f.generic.GenericClass<X>
reason: cannot use '<>' with explicit type parameters for constructor
이렇게 명시적으로 타입 T에 대해서 선언한 상태에서, 타입 X에 대해서는 다이아몬드로 선언하여 컴파일러에게 맡겨 버리면 컴파일이 정상적으로 되지 않으니 유의해야 한다. 따라서, 생성자에 있는 new와 클래스 이름 사이에 타입 이름을 명시적으로 두려면, 다이아몬드를 사용하면 안 된다.
자바의 제네릭을 사용하면서, 발생 가능한 문제 중 하나가 "refiable 하지 않은 varargs 타입(non reifiable varargs type)"이다. 이 문제는 자바에서 제네릭을 사용하지 않는 버전과의 호환성을 위해서 제공했던 기능 때문에 발생한다.
reifiable은 실행시에도 타입 정보가 남아 있는 타입을 의미하고 non-reifiable은 컴파일시 타입 정보가 손실되는 타입을 말한다. 참고로 이 단어는 사전에 없다.
Collections 클래스에는 addAll()이라는 메소드가 있으며 다음과 같이 선언되어 있다.
public static <T> boolean addAll(Collections<? super T> c, T ... elements)
첫 번째 매개 변수에는 추가되는 데이터가 저장되는 Collections을 구현한 클래스의 객체가, 두 번째 매개 변수에는 추가되는 객체들이 나열되도록 되어 있다.
가변 매개 변수를 사용할 경우에 실제로 내부적으로는 다음과 같이 Object의 배열로 처리된다.
public static boolean addAll(java.util.Collection c, java.lang.Object[] elements)
이렇게 Object의 배열로 처리되면, 큰 문제는 발생하지 않지만 잠재적으로 문제가 발생할 수도 있다. 따라서 컴파일 시 경고를 없애기 위해서는 @SafeVarargs라는 어노테이션을 메소드 선언부에 추가하면 된다. 이 Collections 클래스의 addAll() 메소드에도 Java 7부터는 이 어노테이션이 추가되어 있다. 해당 어노테이션은 다음의 경우에만 사용할 수 있다.
그리고 해당 어노테이션은,
에는 컴파일러에서 경고가 발생한다.
Java 7의 변경 사항에 대한 마지막으로 예외에 대해서 살펴보자. 지금까지는 예외를 처리하기 위해서 여러 개의 catch로 잡아주는 코드의 가독성이 떨어지는 작업을 많이 했었다.
지금까지는 각각의 catch 블록을 만들어서 처리하거나, 가장 마지막에 있는 Exception 클래스처럼 Exception 클래스 하나로 catch를 해줬었다.
그런데, Java 7부터는 만약 catch 블록에서 처리하는 방식이 동일하다면 다음과 같이 간단하게 처리할 수 있다.
package f.trycatch;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class TryWithResource {
public void newScanFile(String fileName, String encoding){
Scanner scanner = null;
try{
scanner = new Scanner(new File(fileName), encoding);
System.out.println(scanner.nextLine());
}catch (IllegalArgumentException | FileNotFoundException |
NullPointerException exception){
exception.printStackTrace();
}finally {
if(scanner != null){
scanner.close();
}
}
}
}
각 Exception을 조건식에서 or를 나타내는 파이프(|, 보통 엔터키 위에 있는 역슬래시를 shift와 함께 눌렀을 때 입력되는)로 연결하여 이처럼 간단히 처리해 버릴 수도 있다.
그리고, 자바의 예외에 관해서 추가된 것이 하나 더 있다. 바로 "리소스와 함께 처리하는 try 문장"인데, 영어로 try-with-resource라고 이야기 한다. Java 7에는 AutoCloseable이라는 인터페이스가 추가되었다. try-with-resource를 사용할 때에는 이 인터페이스를 구현한 클래스는 별도로 close()를 호출해 줄 필요가 없다. 즉, finally 문장에서 close()를 처리하기 위해서 코드의 가독성을 떨어지게 할 필요는 없다는 것이다. 앞서 사용한 예제는 다음과 같이 보다 더 간단하게 처리할 수 있다.
public void newScanFileTryWithResource(String fileName, String encoding){
try(Scanner scanner = new Scanner(new File(fileName), encoding)){
System.out.println(scanner.nextLine());
}catch(IllegalArgumentException | FileNotFoundException |
NullPointerException exception){
exception.printStackTrace();
}
}
이와 같이 try 블록의 시작 부분에 catch 처럼 소괄호 안에 필요한 문장을 포함할 수 있다. 이렇게 try의 소괄호 내에 예외가 발생할 수 있는 객체에서 close()를 이용해 닫아야 할 필요가 있을 때 AutoCloseable을 구현한 객체는 finally 문장에서 별도로 처리할 필요가 없는 것을 볼 수 있다.
추가로, 만약 try의 소괄호 내에서 두 개의 객체를 생성할 필요가 있을 때에는 당황할 필요 없이 "세미콜론"으로 그분하여 두 객체를 생성해 주면 된다.
그러면 어떤 클래스들이 AutoCloseable 인터페이스를 구현했는지 살펴보자.
Java 5부터 추가된 java.io.Closeable이라는 인터페이스가 있다. 이 인터페이스를 구현한 클래스들은 명시적으로 close() 메소드를 사용하여 닫아주어야만 했다. 하지만, Java 7부터는 해당 인터페이스는 다음과 같이 선언되어 있다.
public interface Closeable extends AutoCloseable
AutoCloseable이라는 인터페이스를 상속받은 것을 볼 수 있다. 이 인터페이스를 구현한 클래스는 앞에서 살펴본 try-with-resource 문장에서 사용할 수 있다. 많이 사용하는 클래스 중 이 AutoCloseable과 관련있는 클래스들은 다음과 같다.
이 클래스들의 목록은 극히 일부에 속하며, Java 7 API에 있는 클래스 중 매우 많은 클래스가 이 인터페이스를 구현해 놓았기 때문에, 관련된 클래스를 살펴보려면 java.lang 패키지에 선언되어 있는 AutoCloseable 인터페이스의 API를 확인해보길 바란다.