var 는 선언문의 타입을 초기값에서 추론합니다. 타입이 없어지는 것이 아닙니다.
var list = new ArrayList<String>(); // ArrayList<String>
var count = 0; // int. count = "x" 는 컴파일 오류쓸 수 있는 곳과 없는 곳이 정해져 있습니다.
| 가능 | 불가 |
|---|---|
| 초기값 있는 지역 변수 | 초기값 없는 선언 var x; |
| for·for-each 변수 | 필드, 매개변수, 반환 타입 |
| try-with-resources | 람다 var f = () -> 1; |
두 가지를 조심합니다. var list = new ArrayList<>(); 는 ArrayList<Object> 가 됩니다. 다이아몬드와 함께 쓰면 타입 인자를 잃습니다. 그리고 var result = service.process(x); 처럼 우변에서 타입이 안 보이면 읽는 사람이 타입을 추적해야 합니다. 우변에 생성자나 리터럴이 있을 때, 또는 Map.Entry<String, List<Order>> 처럼 타입이 길 때 씁니다.
public record Rect(double w, double h) implements Shape {
Rect { if (w <= 0 || h <= 0) throw new IllegalArgumentException(...); } // 압축 생성자
static Rect square(double side) { return new Rect(side, side); } // 정적 팩토리
public double area() { return w * h; }
}한 줄 헤더에서 생성자, 접근자(w(), h()), equals, hashCode, toString 이 나옵니다. 중급 07 에서 직접 만들던 세 메서드가 전부 자동입니다.
압축 생성자는 매개변수 목록 없이 쓰는 생성자입니다. 필드 대입 직전에 실행되므로 검증과 정규화(스케일 통일, trim)를 여기서 합니다. this.w = w 는 자동으로 붙습니다.
record 가 못 하는 것도 정해져 있습니다.
| 못 하는 것 | 이유 | 대안 |
|---|---|---|
| 헤더 밖 인스턴스 필드 | 상태는 헤더가 전부 | 계산값은 메서드로 |
| setter, 필드 변경 | 모든 필드가 final | moveX(dx) 처럼 새 객체 반환 |
| 클래스 상속 | 암묵적으로 final, extends 불가 |
인터페이스 구현은 가능 |
| JPA 엔티티 | 기본 생성자·가변 필드 필요 | DTO·값 객체에만 |
메서드 안에 지역 record 를 선언할 수도 있습니다. 스트림 중간 결과를 담을 임시 타입이 필요할 때 씁니다.
String sql = """
SELECT o.order_no, o.amount
FROM orders o
WHERE o.status = 'PAID'
""";""" 로 열고 닫습니다. 안에서는 따옴표 이스케이프가 필요 없고 줄바꿈이 그대로 들어갑니다. 규칙은 셋입니다.
""" 의 위치입니다. 내용과 닫는 따옴표 중 가장 왼쪽 열이 0 이 됩니다.""" 를 다음 줄에 두면 끝에 줄바꿈이 붙고, 같은 줄에 두면 붙지 않습니다.\ 는 줄바꿈을 지웁니다. 소스에서는 두 줄, 실행 시에는 한 줄이 됩니다.값 주입은 formatted() 입니다. String.format 과 같지만 문자열 뒤에 붙여 읽기 쉽습니다. SQL 에 사용자 값을 formatted 로 넣으면 SQL 인젝션이므로, SQL 은 텍스트 블록으로 쓰되 값은 바인딩 파라미터(:from, ?)로 넘깁니다.
String kind = switch (day) {
case 1, 7 -> "주말";
case 2, 3, 4, 5, 6 -> "평일";
default -> { String msg = "요일 아님: " + day; yield msg; }
};옛 switch 와 세 가지가 다릅니다.
| 옛 switch 문 | switch 식 |
|---|---|
break 를 빼먹으면 다음 case 로 흘러내림 |
화살표 -> 는 흘러내림 없음 |
| 값을 못 돌려줌, 변수에 대입 필요 | 식이므로 = 오른쪽에 온다 |
| 빠뜨린 case 는 조용히 통과 | 모든 경우를 다루지 않으면 컴파일 오류 |
블록이 필요하면 yield 로 값을 냅니다. 여러 label 은 쉼표로 묶습니다. 완전성 검사가 핵심입니다. int 처럼 경우가 무한한 타입은 default 가 필요하지만, enum 과 sealed 타입은 모든 상수·하위 타입을 나열하면 default 없이 컴파일됩니다. 이때 default 를 일부러 빼야 합니다. 새 경우가 추가됐을 때 컴파일 오류로 알려 주는 것이 목적이기 때문입니다.
if (o instanceof String str && str.length() > 3) { ... } // ① instanceof 패턴
case Circle c -> "원 r=" + c.r(); // ② 타입 패턴
case Rect(double w, double h) when w == h -> "정사각형"; // ③ 해체 패턴 + 가드
case null -> "결과 없음"; // ④ null 케이스①은 instanceof 검사와 캐스팅된 변수 선언을 합칩니다. (String) o 를 쓸 일이 없어집니다. ②는 switch 가 타입으로 분기합니다. ③은 record 의 구성 요소를 바로 변수로 꺼내고, when 뒤 조건이 맞을 때만 그 case 를 탑니다.
case 의 순서가 중요합니다. Circle(double rad) when rad > 5 를 Circle(double rad) 뒤에 두면 절대 도달할 수 없어 컴파일 오류입니다. 특수한 조건을 앞에, 일반 경우를 뒤에 둡니다. ④ case null 이 없으면 switch 에 null 이 들어올 때 NullPointerException 입니다.
public sealed interface PaymentResult permits Approved, Declined, Pending { }
record Approved(String txId, BigDecimal amount) implements PaymentResult {}
record Declined(String reason, boolean retryable) implements PaymentResult {}
record Pending(int etaSeconds) implements PaymentResult {}permits 목록 밖의 클래스는 이 인터페이스를 구현할 수 없습니다. 그래서 컴파일러가 "PaymentResult 는 이 셋뿐" 임을 알고, switch 완전성 검사에 씁니다.
옛 방식은 결과 클래스 하나에 status, reason, eta 필드를 전부 두고 null 로 구분했습니다. 어느 필드가 언제 유효한지는 주석으로만 알 수 있었습니다. sealed + record 는 경우마다 필요한 데이터만 갖습니다. 거절에는 사유가 있고 보류에는 예상 시각이 있으며, 승인에는 둘 다 없습니다.
enum 과의 차이는 경우마다 데이터가 다른가입니다. 상태 이름만 필요하면 enum, 경우마다 다른 필드가 붙으면 sealed 입니다. 새 경우(Refunded)를 추가하면 permits 와 모든 switch 가 컴파일 오류를 내므로 빠뜨릴 수 없습니다.
| 버전 | API | 대체하는 것 |
|---|---|---|
| 11 | String.strip/isBlank/lines/repeat |
trim, 정규식, 반복문 |
| 11 | Files.readString/writeString |
BufferedReader 보일러플레이트 |
| 11 | HttpClient |
HttpURLConnection |
| 15 | String.formatted |
String.format |
| 16 | Stream.toList() |
collect(Collectors.toList()). 결과가 수정 불가 |
| 21 | SequencedCollection: getFirst/getLast/reversed |
get(0), get(size()-1) |
| 21 | 가상 스레드 | 다음 레슨 |