반응형

바쁜 분은 이것만

  1. 메시지는 Cannot ○○ because △△ is null 순서. △△ 자리가 null인 범인이고, ○○는 그 값으로 하려던 일
  2. the return value of "...get(Object)"가 보이면 메서드가 null을 돌려준 것. intValue()가 보이면 null인 Integer를 int로 바꾸다 난 것
  3. 고치는 법: 값을 미리 넣기, "abc".equals(s) 순서, getOrDefault, Objects.requireNonNull / Objects.equals

java.lang.NullPointerException(줄여서 NPE)은 아무것도 가리키지 않는 값, 즉 null에 대고 메서드를 부르거나 필드·배열을 읽으려 할 때 나는 예외입니다. 예전 자바는 메시지 없이 줄 번호만 알려줘서 한 줄에 변수가 여러 개면 누가 null인지 짐작해야 했는데, 요즘 JDK는 무엇이 null이었는지까지 메시지에 적어줍니다. OpenJDK 27에서 상황별로 직접 재현하면서 메시지 읽는 법과 고치는 법을 정리했습니다.

1. 기본형: Cannot invoke "String.length()" because ... is null

public class N1 {
    public static void main(String[] args) {
        String name = null;
        System.out.println("name = " + name);
        System.out.println(name.length());
    }
}
name = null
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null
	at N1.main(N1.java:5)

메시지를 둘로 나눠 읽으면 됩니다.

  • Cannot invoke "String.length()" : String의 length()를 부르려다 실패했다
  • because "<local1>" is null : 그 대상이 null이었다

<local1>은 "이 메서드의 1번 지역 변수"라는 뜻이에요. 0번은 args, 1번이 그다음에 만든 name입니다. 변수 이름 대신 번호가 나오는 이유는 6번에서 따로 확인합니다. 어쨌든 N1.java:5, 5번째 줄의 name이 null이라는 게 바로 보여요. 덤으로, 문자열에 null을 이어 붙이는 4번째 줄은 예외 없이 null이라는 글자로 찍힙니다.

2. 초기화 안 한 필드: Cannot read the array length

public class N2 {
    private String[] items;   // 초기화하지 않은 필드

    void printCount() {
        System.out.println("개수: " + items.length);
    }

    public static void main(String[] args) {
        new N2().printCount();
    }
}
Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "this.items" is null
	at N2.printCount(N2.java:5)
	at N2.main(N2.java:9)

객체의 필드는 따로 값을 넣지 않으면 기본값이 들어가는데, 배열·String 같은 참조형의 기본값은 null입니다. 그래서 items.length에서 "배열 길이를 읽을 수 없다"고 나와요. 필드는 지역 변수와 달리 "this.items"처럼 이름이 그대로 보입니다. 선언할 때 private String[] items = new String[0];처럼 빈 배열이라도 넣어 두거나 생성자에서 채우면 됩니다.

3. 배열 칸이 비어 있을 때: "<local1>[0]" is null

public class N3 {
    public static void main(String[] args) {
        String[] names = new String[3];   // 칸만 만들고 값은 안 넣음
        names[1] = "kim";
        System.out.println(names[1].length());
        System.out.println(names[0].length());
    }
}
3
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>[0]" is null
	at N3.main(N3.java:6)

new String[3]은 칸 3개를 만들 뿐, 각 칸은 null입니다. 값을 넣은 1번 칸은 괜찮고 안 넣은 0번 칸에서 터졌어요. 메시지가 [0]까지 짚어주니 어느 칸인지 바로 알 수 있습니다.

2차원 배열도 같은 이유로 생깁니다.

public class N4 {
    public static void main(String[] args) {
        int[][] grid = new int[2][];   // 행만 만들고 각 행 배열은 안 만듦
        System.out.println("grid[0] = " + grid[0]);
        System.out.println(grid[0][0]);
    }
}
grid[0] = null
Exception in thread "main" java.lang.NullPointerException: Cannot load from int array because "<local1>[0]" is null
	at N4.main(N4.java:5)

Cannot load from int array는 "int 배열에서 값을 꺼낼 수 없다"는 뜻이에요. new int[2][]처럼 뒤 칸 수를 비우면 행 배열이 만들어지지 않아 grid[0]이 null입니다. new int[2][3]으로 만들거나 grid[0] = new int[3];처럼 행마다 따로 만들어 주세요.

4. Map.get이 null을 돌려줄 때: the return value of ...

import java.util.HashMap;
import java.util.Map;

public class N5 {
    public static void main(String[] args) {
        Map<String, String> phone = new HashMap<>();
        phone.put("kim", "010-1111-2222");
        System.out.println(phone.get("kim").length());
        System.out.println(phone.get("lee").length());
    }
}
13
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because the return value of "java.util.Map.get(Object)" is null
	at N5.main(N5.java:9)

이번엔 변수 이름 대신 the return value of "java.util.Map.get(Object)", 즉 get()이 돌려준 값이 null이었다고 나옵니다. Map.get()은 없는 키를 넣으면 예외 대신 null을 돌려주기 때문에, 키가 있는지 모르는 상태에서 바로 뒤에 메서드를 이어 부르면 이렇게 됩니다.

직접 만든 메서드도 똑같이 표시돼요.

public class N8 {
    static String findName(int id) {
        if (id == 1) {
            return "kim";
        }
        return null;   // 못 찾으면 null
    }

    public static void main(String[] args) {
        System.out.println(findName(1).toUpperCase());
        System.out.println(findName(2).toUpperCase());
    }
}
KIM
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because the return value of "N8.findName(int)" is null
	at N8.main(N8.java:11)

"N8.findName(int)"라고 메서드 이름이 나오니, 그 메서드가 어떤 경우에 null을 돌려주는지 보러 가면 됩니다.

5. 언박싱: Cannot invoke "java.lang.Integer.intValue()"

코드에 intValue()를 쓴 적이 없는데 이 메시지가 나오면 거의 이 경우예요.

import java.util.HashMap;
import java.util.Map;

public class N6 {
    public static void main(String[] args) {
        Map<String, Integer> stock = new HashMap<>();
        stock.put("apple", 3);
        int apple = stock.get("apple");
        System.out.println("apple = " + apple);
        int banana = stock.get("banana");
        System.out.println("banana = " + banana);
    }
}
apple = 3
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
	at N6.main(N6.java:10)

Integer(객체)를 int(기본형) 변수에 넣으면 자바가 몰래 intValue()를 불러 숫자를 꺼냅니다. 이걸 언박싱이라고 해요. 없는 키라 get()이 null을 돌려줬고, null에서 intValue()를 부르다 터진 겁니다. Map이 아니어도 마찬가지예요.

public class N7 {
    public static void main(String[] args) {
        Integer count = null;   // 값이 아직 없음
        int total = count + 1;
        System.out.println(total);
    }
}
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "<local1>" is null
	at N7.main(N7.java:4)

null이 될 수 있는 값은 int 대신 Integer로 받아 확인하거나, 처음부터 기본값을 넣어 두세요. Map에서 숫자를 꺼낼 땐 아래 8번의 getOrDefault가 편해요.

6. 변수 이름이 나올 때와 <local1>이 나올 때

똑같은 N1.java를 실행 방법만 바꿔 돌려 봤습니다.

$ java N1.java                       # 소스 파일 바로 실행
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null

$ javac N1.java && java N1           # 옵션 없이 컴파일
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null

$ javac -g N1.java && java N1        # -g 붙여 컴파일
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null

(위는 출력의 예외 줄만 모은 것이고, 각각의 전체 출력은 name = null과 at N1.main(N1.java:5)가 앞뒤로 붙습니다.)

지역 변수 이름은 컴파일할 때 -g 옵션을 줘야 .class 파일에 남습니다. 옵션 없는 javac는 줄 번호와 파일 이름만 남기고 변수 이름은 빼기 때문에 JVM이 <local1>처럼 번호로만 알려줘요. java 파일명.java로 바로 실행하는 방식도 OpenJDK 27에서 확인해 보니 <local1>로 나왔습니다. 메서드 매개변수도 마찬가지예요.

public class N15 {
    static int len(String text) {
        return text.length();
    }

    public static void main(String[] args) {
        System.out.println(len(null));
    }
}
$ java N15.java
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<parameter1>" is null
	at N15.len(N15.java:3)
	at N15.main(N15.java:7)

$ javac -g N15.java && java N15
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "text" is null
	at N15.len(N15.java:3)
	at N15.main(N15.java:7)

번호로 나와도 당황할 필요는 없습니다. 줄 번호가 있으니 그 줄에서 메서드를 부르는 변수를 찾으면 돼요. 필드(this.items)와 메서드 반환값(Map.get(Object))은 옵션과 상관없이 이름이 나옵니다.

7. 연쇄 호출 a.getB().getC()

public class N9 {
    static class Address {
        String city;
        String getCity() { return city; }
    }

    static class User {
        Address address;
        Address getAddress() { return address; }
    }

    public static void main(String[] args) {
        User user = new User();   // address를 안 넣음
        System.out.println(user.getAddress().getCity().length());
    }
}
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "N9$Address.getCity()" because the return value of "N9$User.getAddress()" is null
	at N9.main(N9.java:14)

한 줄에 점(.)이 여러 개면 예전엔 어디서 끊겼는지 알 수 없었는데, 지금은 getAddress()가 null을 돌려줘서 getCity()를 못 불렀다고 정확히 짚어 줍니다. N9$Address의 $는 N9 안에 든 클래스라는 표시예요. 중간 값을 변수로 받아 확인하면 안전합니다.

public class N16 {
    static class Address {
        String city;
        String getCity() { return city; }
    }

    static class User {
        Address address;
        Address getAddress() { return address; }
    }

    public static void main(String[] args) {
        User user = new User();
        Address address = user.getAddress();
        if (address == null || address.getCity() == null) {
            System.out.println("주소 정보 없음");
        } else {
            System.out.println(address.getCity().length());
        }
    }
}
주소 정보 없음

||는 앞쪽이 참이면 뒤쪽을 계산하지 않기 때문에 address가 null일 때 address.getCity()는 실행되지 않아요.

8. 원인별 고치는 법

equals는 상수를 앞에

public class N10 {
    public static void main(String[] args) {
        String input = null;
        System.out.println("abc".equals(input));
        System.out.println(input.equals("abc"));
    }
}
false
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "<local1>" is null
	at N10.main(N10.java:5)

"abc"는 절대 null이 아니니 앞에 두면 input이 null이어도 그냥 false가 나옵니다.

Objects.equals, requireNonNullElse, toString

import java.util.Objects;

public class N12 {
    public static void main(String[] args) {
        String a = null;
        String b = null;
        String c = "abc";
        System.out.println(Objects.equals(a, b));
        System.out.println(Objects.equals(a, c));
        System.out.println(Objects.equals(c, "abc"));
        System.out.println(Objects.requireNonNullElse(a, "(이름 없음)"));
        System.out.println(Objects.toString(a, "(이름 없음)"));
    }
}
true
false
true
(이름 없음)
(이름 없음)

두 변수 모두 null일 수 있으면 Objects.equals(a, b)를 씁니다. 둘 다 null이면 true, 하나만 null이면 false를 돌려주고 예외는 나지 않아요. null일 때 대신 쓸 값이 있으면 Objects.requireNonNullElse(값, 대신할값)이나 Objects.toString(값, 대신할문자열)이 짧습니다.

Objects.requireNonNull로 일찍 막기

import java.util.Objects;

public class N11 {
    static class Member {
        private final String name;

        Member(String name) {
            this.name = Objects.requireNonNull(name, "name은 null이면 안 됩니다");
        }
    }

    public static void main(String[] args) {
        Member ok = new Member("kim");
        System.out.println("첫 번째 회원 생성 완료");
        Member bad = new Member(null);
    }
}
첫 번째 회원 생성 완료
Exception in thread "main" java.lang.NullPointerException: name은 null이면 안 됩니다
	at java.base/java.util.Objects.requireNonNull(Objects.java:246)
	at N11$Member.<init>(N11.java:8)
	at N11.main(N11.java:15)

예외가 나는 건 같지만, null이 들어오는 순간 내가 정한 메시지로 멈춥니다. null을 품은 객체가 한참 돌아다니다 엉뚱한 곳에서 터지는 것보다 원인을 찾기 훨씬 쉬워요. 트레이스의 <init>은 생성자라는 뜻입니다.

Map은 getOrDefault 또는 null 확인

import java.util.HashMap;
import java.util.Map;

public class N13 {
    public static void main(String[] args) {
        Map<String, Integer> stock = new HashMap<>();
        stock.put("apple", 3);
        int banana = stock.getOrDefault("banana", 0);
        System.out.println("banana = " + banana);

        Map<String, String> phone = new HashMap<>();
        String lee = phone.get("lee");
        if (lee != null) {
            System.out.println(lee.length());
        } else {
            System.out.println("lee 번호 없음");
        }
    }
}
banana = 0
lee 번호 없음

5번에서 터졌던 코드를 getOrDefault("banana", 0)으로 바꾸니 키가 없을 때 0이 들어와 언박싱 예외가 사라졌습니다. 기본값이 마땅치 않으면 꺼낸 값을 변수에 받아 null인지 먼저 확인하세요.

null 대신 Optional 돌려주기 (짧게)

import java.util.Optional;

public class N14 {
    static Optional<String> findName(int id) {
        return id == 1 ? Optional.of("kim") : Optional.empty();
    }

    public static void main(String[] args) {
        System.out.println(findName(1).map(String::toUpperCase).orElse("(없음)"));
        System.out.println(findName(2).map(String::toUpperCase).orElse("(없음)"));
    }
}
KIM
(없음)

4번의 findName처럼 "없을 수도 있는" 값을 돌려주는 메서드라면 반환 타입을 Optional로 바꾸는 방법도 있습니다. 부르는 쪽이 orElse로 없는 경우를 처리하게 되니 그냥 .toUpperCase()를 붙였다 터질 일이 줄어요. Optional 사용법은 자바 Optional 사용법 정리에서 따로 다뤘습니다.

정리

메시지 / 상황 원인과 고치는 법
Cannot invoke "X.m()" because "변수" is null 그 변수에 값이 없음 → 값을 넣었는지, 어디서 null이 들어왔는지 확인
"this.필드" is null / Cannot read the array length 초기화 안 한 필드 → 선언 때나 생성자에서 값 넣기
"arr[0]" is null / Cannot load from int array 배열 칸·2차원 배열의 행이 비어 있음 → 칸마다 값(배열) 넣기
the return value of "Map.get(Object)" is null 없는 키 → getOrDefault 또는 null 확인
Cannot invoke "java.lang.Integer.intValue()" null인 Integer를 int로 언박싱 → 기본값 넣기
a.getB().getC()에서 the return value of "getB()" 중간 메서드가 null 반환 → 변수로 받아 확인
"<local1>", "<parameter1>" 변수 이름 정보 없이 컴파일됨 → 줄 번호로 찾거나 javac -g
문자열 비교 "abc".equals(s), Objects.equals(a, b)

참고: Oracle Java SE API 문서 — NullPointerException, Objects(equals, requireNonNull, requireNonNullElse, toString), Map.get / getOrDefault, Optional / OpenJDK JEP 358: Helpful NullPointerExceptions / javac 도구 문서(-g 옵션) / JEP 330: Launch Single-File Source-Code Programs

반응형

+ Recent posts