버그가 있는 Java 코드: Java 개발자가 저지르는 가장 일반적인 실수 10가지

게시 됨: 2022-03-11

Java는 처음에 대화형 텔레비전용으로 개발된 프로그래밍 언어이지만 시간이 지남에 따라 소프트웨어를 사용할 수 있는 모든 곳에서 널리 보급되었습니다. C 또는 C++, 가비지 수집 및 아키텍처에 구애받지 않는 가상 머신과 같은 다른 언어의 복잡성을 없애고 객체 ​​지향 프로그래밍의 개념으로 설계된 Java는 새로운 프로그래밍 방식을 만들었습니다. 또한 학습 곡선이 완만하며 거의 항상 사실인 "한 번 작성, 모든 곳에서 실행"이라는 자체 모토를 성공적으로 준수하는 것으로 보입니다. 그러나 Java 문제는 여전히 존재합니다. 가장 흔한 실수라고 생각되는 10가지 Java 문제를 다룰 것입니다.

일반적인 실수 #1: 기존 라이브러리 무시

Java 개발자가 Java로 작성된 수많은 라이브러리를 무시하는 것은 확실히 실수입니다. 바퀴를 재발명하기 전에 사용 가능한 라이브러리를 검색하십시오. 많은 라이브러리가 수년 동안 개선되었으며 무료로 사용할 수 있습니다. logback 및 Log4j와 같은 로깅 라이브러리 또는 Netty 또는 Akka와 같은 네트워크 관련 라이브러리가 될 수 있습니다. Joda-Time과 같은 일부 라이브러리는 사실상의 표준이 되었습니다.

다음은 이전 프로젝트 중 하나에서 개인적인 경험입니다. HTML 이스케이프를 담당하는 코드 부분은 처음부터 작성되었습니다. 몇 년 동안 잘 작동했지만 결국 사용자 입력이 발생하여 무한 루프로 회전했습니다. 서비스가 응답하지 않는 것을 발견한 사용자는 동일한 입력으로 재시도를 시도했습니다. 결국 이 응용 프로그램에 할당된 서버의 모든 CPU가 이 무한 루프에 의해 점유되었습니다. 이 순진한 HTML 이스케이프 도구의 작성자가 Google Guava의 HtmlEscapers 와 같이 HTML 이스케이프에 사용할 수 있는 잘 알려진 라이브러리 중 하나를 사용하기로 결정했다면 이런 일은 일어나지 않았을 것입니다. 최소한 커뮤니티가 있는 가장 인기 있는 라이브러리의 경우 이 라이브러리에 대한 커뮤니티에서 오류를 더 일찍 발견하고 수정했을 것입니다.

일반적인 실수 #2: 스위치 케이스 블록에서 'break' 키워드 누락

이러한 Java 문제는 매우 난처할 수 있으며 때로는 프로덕션 환경에서 실행할 때까지 발견되지 않은 채로 남아 있습니다. switch 문의 폴스루 동작은 종종 유용합니다. 그러나 그러한 행동이 바람직하지 않을 때 "break" 키워드를 놓치면 재앙적인 결과를 초래할 수 있습니다. 아래 코드 예제에서 "case 0"에 "break"를 넣는 것을 잊은 경우 프로그램은 "Zero" 다음에 "One"을 작성합니다. 왜냐하면 여기 내부의 제어 흐름은 전체 "switch" 문을 통과할 때까지 그것은 "휴식"에 도달합니다. 예를 들어:

 public static void switchCasePrimer() { int caseIndex = 0; switch (caseIndex) { case 0: System.out.println("Zero"); case 1: System.out.println("One"); break; case 2: System.out.println("Two"); break; default: System.out.println("Default"); } }

대부분의 경우 더 깨끗한 솔루션은 다형성을 사용하고 특정 동작이 있는 코드를 별도의 클래스로 이동하는 것입니다. 이와 같은 Java 실수는 FindBugs 및 PMD와 같은 정적 코드 분석기를 사용하여 감지할 수 있습니다.

흔한 실수 #3: 무료 리소스를 잊어버리기

프로그램이 파일이나 네트워크 연결을 열 때마다 Java 초보자는 사용을 마치면 리소스를 해제하는 것이 중요합니다. 그러한 자원에 대한 작업 중에 예외가 발생하는 경우 유사한 주의를 기울여야 합니다. FileInputStream에 가비지 수집 이벤트에서 close() 메서드를 호출하는 종료자가 있다고 주장할 수 있습니다. 그러나 가비지 수집 주기가 언제 시작될지 확신할 수 없기 때문에 입력 스트림은 무기한 기간 동안 컴퓨터 리소스를 소비할 수 있습니다. 사실, 특히 이 경우를 위해 Java 7에 도입된 정말 유용하고 깔끔한 문이 있습니다. try-with-resources라고 합니다.

 private static void printFileJava7() throws IOException { try(FileInputStream input = new FileInputStream("file.txt")) { int data = input.read(); while(data != -1){ System.out.print((char) data); data = input.read(); } } }

이 문은 AutoClosable 인터페이스를 구현하는 모든 개체와 함께 사용할 수 있습니다. 명령문이 끝날 때 각 리소스가 닫히도록 합니다.

관련: 8가지 필수 Java 인터뷰 질문

일반적인 실수 #4: 메모리 누수

Java는 자동 메모리 관리를 사용하며, 수동으로 메모리를 할당하고 해제하는 것을 잊어도 안심이 되지만 Java 개발자가 처음 응용 프로그램에서 메모리가 사용되는 방식을 알아야 한다는 의미는 아닙니다. 메모리 할당 문제는 여전히 가능합니다. 프로그램이 더 이상 필요하지 않은 개체에 대한 참조를 생성하는 한 해제되지 않습니다. 어떤 면에서 우리는 여전히 이것을 메모리 누수라고 부를 수 있습니다. Java의 메모리 누수는 다양한 방식으로 발생할 수 있지만 가장 일반적인 이유는 객체 참조가 영구적이기 때문입니다. 가비지 수집기가 객체에 대한 참조가 있는 동안에는 힙에서 객체를 제거할 수 없기 때문입니다. 일부 개체 컬렉션을 포함하는 정적 필드로 클래스를 정의하고 컬렉션이 더 이상 필요하지 않은 후에 해당 정적 필드를 null로 설정하는 것을 잊어서 이러한 참조를 만들 수 있습니다. 정적 필드는 GC 루트로 간주되며 수집되지 않습니다.

이러한 메모리 누수의 또 다른 잠재적인 이유는 서로를 참조하는 개체 그룹으로 인해 순환 종속성을 유발하여 가비지 수집기가 교차 종속성 참조가 있는 이러한 개체가 필요한지 여부를 결정할 수 없습니다. 또 다른 문제는 JNI를 사용할 때 비힙 메모리의 누수입니다.

기본 누출 예제는 다음과 같을 수 있습니다.

 final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); final Deque<BigDecimal> numbers = new LinkedBlockingDeque<>(); final BigDecimal divisor = new BigDecimal(51); scheduledExecutorService.scheduleAtFixedRate(() -> { BigDecimal number = numbers.peekLast(); if (number != null && number.remainder(divisor).byteValue() == 0) { System.out.println("Number: " + number); System.out.println("Deque size: " + numbers.size()); } }, 10, 10, TimeUnit.MILLISECONDS); scheduledExecutorService.scheduleAtFixedRate(() -> { numbers.add(new BigDecimal(System.currentTimeMillis())); }, 10, 10, TimeUnit.MILLISECONDS); try { scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS); } catch (InterruptedException e) { e.printStackTrace(); }

이 예에서는 두 개의 예약된 작업을 만듭니다. 첫 번째 작업은 "numbers"라는 데크에서 마지막 숫자를 가져와서 숫자가 51로 나누어 떨어지는 경우에 숫자와 데크 크기를 출력합니다. 두 번째 작업은 데크에 숫자를 넣습니다. 두 작업 모두 고정된 속도로 예약되고 10ms마다 실행됩니다. 코드가 실행되면 데크의 크기가 영구적으로 증가하는 것을 볼 수 있습니다. 이것은 결국 데크가 사용 가능한 모든 힙 메모리를 소비하는 객체로 채워지게 합니다. 이 프로그램의 의미를 유지하면서 이것을 방지하기 위해 "pollLast"라는 데크에서 숫자를 가져오는 다른 방법을 사용할 수 있습니다. "peekLast" 메서드와 달리 "pollLast"는 요소를 반환하고 데크에서 제거하지만 "peekLast"는 마지막 요소만 반환합니다.

Java의 메모리 누수에 대해 자세히 알아보려면 이 문제를 설명하는 기사를 참조하십시오.

일반적인 실수 #5: 과도한 쓰레기 할당

프로그램이 수명이 짧은 개체를 많이 만들 때 과도한 가비지 할당이 발생할 수 있습니다. 가비지 수집기는 지속적으로 작동하여 메모리에서 불필요한 개체를 제거하여 애플리케이션 성능에 부정적인 영향을 미칩니다. 간단한 예:

 String oneMillionHello = ""; for (int i = 0; i < 1000000; i++) { oneMillionHello = oneMillionHello + "Hello!"; } System.out.println(oneMillionHello.substring(0, 6));

Java 개발에서 문자열은 변경할 수 없습니다. 따라서 각 반복마다 새 문자열이 생성됩니다. 이 문제를 해결하려면 변경 가능한 StringBuilder를 사용해야 합니다.

 StringBuilder oneMillionHelloSB = new StringBuilder(); for (int i = 0; i < 1000000; i++) { oneMillionHelloSB.append("Hello!"); } System.out.println(oneMillionHelloSB.toString().substring(0, 6));

첫 번째 버전은 실행하는 데 상당한 시간이 필요하지만 StringBuilder를 사용하는 버전은 훨씬 적은 시간으로 결과를 생성합니다.

일반적인 실수 #6: 필요 없는 Null 참조 사용

null을 과도하게 사용하지 않는 것이 좋습니다. 예를 들어 NullPointerException을 방지하는 데 도움이 될 수 있으므로 null 대신 메서드에서 빈 배열이나 컬렉션을 반환하는 것이 좋습니다.

아래와 같이 다른 방법에서 얻은 컬렉션을 순회하는 다음 방법을 고려하십시오.

 List<String> accountIds = person.getAccountIds(); for (String accountId : accountIds) { processAccount(accountId); }

개인에게 계정이 없을 때 getAccountIds()가 null을 반환하면 NullPointerException이 발생합니다. 이 문제를 해결하려면 null 검사가 필요합니다. 그러나 null 대신 빈 목록을 반환하면 NullPointerException이 더 이상 문제가 되지 않습니다. 게다가, 변수 accountIds를 null 체크할 필요가 없기 때문에 코드가 더 깔끔합니다.

null을 피하려는 다른 경우를 처리하기 위해 다른 전략을 사용할 수 있습니다. 이러한 전략 중 하나는 빈 객체 또는 일부 값의 랩이 될 수 있는 Optional 유형을 사용하는 것입니다.

 Optional<String> optionalString = Optional.ofNullable(nullableString); if(optionalString.isPresent()) { System.out.println(optionalString.get()); }

실제로 Java 8은 보다 간결한 솔루션을 제공합니다.

 Optional<String> optionalString = Optional.ofNullable(nullableString); optionalString.ifPresent(System.out::println);

선택적 유형은 버전 8부터 Java의 일부였지만 기능 프로그래밍의 세계에서는 오랫동안 잘 알려져 왔습니다. 이전에는 이전 버전의 Java용 Google Guava에서 사용할 수 있었습니다.

일반적인 실수 #7: 예외 무시

예외를 처리하지 않은 채로 두는 경우가 많습니다. 그러나 초보자와 숙련된 Java 개발자 모두에게 가장 좋은 방법은 이를 처리하는 것입니다. 예외는 의도적으로 발생하므로 대부분의 경우 이러한 예외를 일으키는 문제를 해결해야 합니다. 이러한 이벤트를 간과하지 마십시오. 필요한 경우 다시 던지거나, 사용자에게 오류 대화 상자를 표시하거나, 로그에 메시지를 추가할 수 있습니다. 최소한 다른 개발자에게 그 이유를 알리기 위해 예외가 처리되지 않은 이유를 설명해야 합니다.

 selfie = person.shootASelfie(); try { selfie.show(); } catch (NullPointerException e) { // Maybe, invisible man. Who cares, anyway? }

예외의 중요성을 강조하는 더 명확한 방법은 다음과 같이 이 메시지를 예외의 변수 이름으로 인코딩하는 것입니다.

 try { selfie.delete(); } catch (NullPointerException unimportant) { }

일반적인 실수 #8: 동시 수정 예외

이 예외는 iterator 객체에서 제공하는 것과 다른 메서드를 사용하여 컬렉션을 반복하는 동안 컬렉션이 수정될 때 발생합니다. 예를 들어 모자 목록이 있고 귀 덮개가 있는 모자를 모두 제거하려고 합니다.

 List<IHat> hats = new ArrayList<>(); hats.add(new Ushanka()); // that one has ear flaps hats.add(new Fedora()); hats.add(new Sombrero()); for (IHat hat : hats) { if (hat.hasEarFlaps()) { hats.remove(hat); } }

이 코드를 실행하면 코드가 컬렉션을 반복하는 동안 컬렉션을 수정하므로 "ConcurrentModificationException"이 발생합니다. 동일한 목록으로 작업하는 여러 스레드 중 하나가 컬렉션을 수정하는 동안 다른 스레드가 컬렉션을 반복하는 경우 동일한 예외가 발생할 수 있습니다. 여러 스레드에서 컬렉션을 동시에 수정하는 것은 자연스러운 일이지만 동기화 잠금, 동시 수정을 위해 채택된 특수 컬렉션 등과 같은 동시 프로그래밍 도구 상자의 일반적인 도구로 처리해야 합니다. 이 Java 문제를 해결하는 방법에는 미묘한 차이가 있습니다. 단일 스레드의 경우와 다중 스레드의 경우. 다음은 단일 스레드 시나리오에서 처리할 수 있는 몇 가지 방법에 대한 간략한 설명입니다.

개체를 수집하고 다른 루프에서 제거

다른 루프 내에서 나중에 제거하기 위해 목록에서 귀 덮개가 있는 모자를 수집하는 것은 분명한 해결책이지만 제거할 모자를 저장하기 위한 추가 컬렉션이 필요합니다.

 List<IHat> hatsToRemove = new LinkedList<>(); for (IHat hat : hats) { if (hat.hasEarFlaps()) { hatsToRemove.add(hat); } } for (IHat hat : hatsToRemove) { hats.remove(hat); }

Iterator.remove 메소드 사용

이 접근 방식은 더 간결하며 추가 컬렉션을 생성할 필요가 없습니다.

 Iterator<IHat> hatIterator = hats.iterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.remove(); } }

ListIterator의 메소드 사용

수정된 컬렉션이 List 인터페이스를 구현할 때 목록 반복자를 사용하는 것이 적절합니다. ListIterator 인터페이스를 구현하는 반복자는 제거 작업뿐만 아니라 추가 및 설정 작업도 지원합니다. ListIterator는 Iterator 인터페이스를 구현하므로 예제는 Iterator remove 메소드와 거의 동일하게 보입니다. 유일한 차이점은 모자 반복자의 유형과 "listIterator()" 메서드를 사용하여 해당 반복자를 얻는 방법입니다. 아래 스니펫은 "ListIterator.remove" 및 "ListIterator.add" 메서드를 사용하여 귀 덮개가 있는 각 모자를 솜브레로로 교체하는 방법을 보여줍니다.

 IHat sombrero = new Sombrero(); ListIterator<IHat> hatIterator = hats.listIterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.remove(); hatIterator.add(sombrero); } }

ListIterator를 사용하면 remove 및 add 메소드 호출을 set에 대한 단일 호출로 대체할 수 있습니다.

 IHat sombrero = new Sombrero(); ListIterator<IHat> hatIterator = hats.listIterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.set(sombrero); // set instead of remove and add } }

Java 8에 도입된 스트림 메소드 사용 Java 8을 사용하면 프로그래머는 컬렉션을 스트림으로 변환하고 일부 기준에 따라 해당 스트림을 필터링할 수 있습니다. 다음은 스트림 API가 모자를 필터링하고 "ConcurrentModificationException"을 방지하는 데 어떻게 도움이 되는지에 대한 예입니다.

 hats = hats.stream().filter((hat -> !hat.hasEarFlaps())) .collect(Collectors.toCollection(ArrayList::new));

"Collectors.toCollection" 메서드는 필터링된 모자가 있는 새 ArrayList를 만듭니다. 필터링 조건이 많은 수의 항목에 의해 충족되어 큰 ArrayList가 생성되는 경우 문제가 될 수 있습니다. 따라서 주의해서 사용해야 합니다. Java 8에 있는 List.removeIf 메소드 사용 Java 8에서 사용할 수 있는 또 다른 솔루션은 분명히 가장 간결한 "removeIf" 메소드를 사용하는 것입니다.

 hats.removeIf(IHat::hasEarFlaps);

그게 다야 내부적으로는 "Iterator.remove"를 사용하여 동작을 수행합니다.

전문 컬렉션 사용

처음에 "ArrayList" 대신 "CopyOnWriteArrayList"를 사용하기로 결정했다면 "CopyOnWriteArrayList"는 변경되지 않는 수정 메서드(예: 설정, 추가 및 제거)를 제공하기 때문에 전혀 문제가 없었을 것입니다. 컬렉션의 배킹 배열이 아니라 새로운 수정된 버전을 만듭니다. 이렇게 하면 "ConcurrentModificationException"의 위험 없이 컬렉션의 원본 버전과 수정 버전을 동시에 반복할 수 있습니다. 그 컬렉션의 단점은 명백합니다. 수정될 때마다 새로운 컬렉션이 생성된다는 것입니다.

"CopyOnWriteSet" 및 "ConcurrentHashMap"과 같이 다른 경우에 맞게 조정된 다른 컬렉션이 있습니다.

동시 컬렉션 수정에서 발생할 수 있는 또 다른 실수는 컬렉션에서 스트림을 만들고 스트림 반복 중에 백업 컬렉션을 수정하는 것입니다. 스트림에 대한 일반적인 규칙은 스트림 쿼리 중에 기본 컬렉션의 수정을 피하는 것입니다. 다음 예는 스트림을 처리하는 잘못된 방법을 보여줍니다.

 List<IHat> filteredHats = hats.stream().peek(hat -> { if (hat.hasEarFlaps()) { hats.remove(hat); } }).collect(Collectors.toCollection(ArrayList::new));

peek 메서드는 모든 요소를 ​​수집하고 각 요소에 대해 제공된 작업을 수행합니다. 여기에서 작업은 잘못된 기본 목록에서 요소를 제거하려고 시도하고 있습니다. 이를 방지하려면 위에서 설명한 몇 가지 방법을 시도하십시오.

일반적인 실수 #9: 계약 위반

때로는 표준 라이브러리나 타사 공급업체에서 제공하는 코드가 제대로 작동하도록 하기 위해 준수해야 하는 규칙에 의존합니다. 예를 들어, hashCode 및 equals 계약을 따를 경우 Java 컬렉션 프레임워크의 컬렉션 집합과 hashCode 및 equals 메서드를 사용하는 다른 클래스에 대해 작업을 보장할 수 있습니다. 계약을 어기는 것은 항상 예외를 일으키거나 코드 컴파일을 중단시키는 종류의 오류가 아닙니다. 때로는 위험 징후 없이 애플리케이션 동작을 변경하기 때문에 더 까다롭습니다. 잘못된 코드는 프로덕션 릴리스에 들어가 원치 않는 결과를 초래할 수 있습니다. 여기에는 잘못된 UI 동작, 잘못된 데이터 보고, 낮은 애플리케이션 성능, 데이터 손실 등이 포함될 수 있습니다. 다행히도 이러한 치명적인 버그는 자주 발생하지 않습니다. 나는 이미 hashCode와 equals 계약을 언급했습니다. HashMap 및 HashSet과 같은 해싱 및 비교 개체에 의존하는 컬렉션에서 사용됩니다. 간단히 말해서 계약에는 두 가지 규칙이 있습니다.

  • 두 객체가 같으면 해시 코드가 같아야 합니다.
  • 두 개체의 해시 코드가 같으면 같을 수도 있고 같지 않을 수도 있습니다.

계약의 첫 번째 규칙을 위반하면 해시맵에서 개체를 검색하는 동안 문제가 발생합니다. 두 번째 규칙은 해시 코드가 동일한 객체가 반드시 같지는 않다는 것을 나타냅니다. 첫 번째 규칙을 어겼을 때의 효과를 살펴보겠습니다.

 public static class Boat { private String name; Boat(String name) { this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Boat boat = (Boat) o; return !(name != null ? !name.equals(boat.name) : boat.name != null); } @Override public int hashCode() { return (int) (Math.random() * 5000); } }

보시다시피, Boat 클래스는 equals 및 hashCode 메소드를 재정의했습니다. 그러나 hashCode는 호출될 때마다 동일한 개체에 대해 임의의 값을 반환하기 때문에 계약을 위반했습니다. 다음 코드는 해시 집합에서 "Enterprise"라는 이름의 보트를 찾지 못할 가능성이 높습니다. 이러한 유형의 보트를 더 일찍 추가했음에도 불구하고:

 public static void main(String[] args) { Set<Boat> boats = new HashSet<>(); boats.add(new Boat("Enterprise")); System.out.printf("We have a boat named 'Enterprise' : %b\n", boats.contains(new Boat("Enterprise"))); }

계약의 또 다른 예는 finalize 메소드입니다. 다음은 해당 기능을 설명하는 공식 Java 문서의 인용문입니다.

finalize의 일반적인 계약은 JavaTM 가상 머신이 다음의 결과를 제외하고 (아직 죽지 않은) 모든 스레드에서 이 객체에 액세스할 수 있는 수단이 더 이상 없다고 판단한 경우 호출된다는 것입니다. 완료될 준비가 된 다른 객체 또는 클래스의 종료에 의해 취해진 작업. finalize 메서드는 이 객체를 다른 스레드에서 다시 사용할 수 있도록 하는 것을 포함하여 모든 작업을 수행할 수 있습니다. 그러나 finalize의 일반적인 목적은 개체를 취소할 수 없게 버리기 전에 정리 작업을 수행하는 것입니다. 예를 들어 입력/출력 연결을 나타내는 개체의 finalize 메서드는 개체가 영구적으로 삭제되기 전에 연결을 끊기 위해 명시적 I/O 트랜잭션을 수행할 수 있습니다.

파일 핸들러와 같은 리소스를 해제하기 위해 finalize 메소드를 사용하기로 결정할 수 있지만 이는 잘못된 생각입니다. 이는 가비지 수집 중에 호출되고 GC 시간을 결정할 수 없기 때문에 finalize가 호출될 시간에 대한 보장이 없기 때문입니다.

일반적인 실수 #10: 매개변수화된 유형 대신 원시 유형 사용

Java 사양에 따르면 원시 유형은 매개변수화되지 않은 유형이거나 R의 수퍼클래스 또는 수퍼인터페이스에서 상속되지 않은 클래스 R의 비정적 멤버입니다. 일반 유형이 Java에 도입될 때까지 원시 유형에 대한 대안이 없었습니다. . 버전 1.5부터 제네릭 프로그래밍을 지원하며 제네릭은 의심할 여지 없이 크게 개선되었습니다. 그러나 이전 버전과의 호환성 문제로 인해 유형 시스템을 잠재적으로 손상시킬 수 있는 함정이 남아 있습니다. 다음 예를 살펴보겠습니다.

 List listOfNumbers = new ArrayList(); listOfNumbers.add(10); listOfNumbers.add("Twenty"); listOfNumbers.forEach(n -> System.out.println((int) n * 2));

여기에 원시 ArrayList로 정의된 숫자 목록이 있습니다. 유형이 유형 매개변수로 지정되지 않았기 때문에 모든 객체를 추가할 수 있습니다. 그러나 마지막 줄에서 우리는 요소를 int로 캐스팅하고 두 배로 늘리고 두 배의 숫자를 표준 출력으로 인쇄합니다. 이 코드는 오류 없이 컴파일되지만 일단 실행하면 문자열을 정수로 변환하려고 시도했기 때문에 런타임 예외가 발생합니다. 분명히, 유형 시스템은 필요한 정보를 숨기면 안전한 코드를 작성하는 데 도움이 될 수 없습니다. 문제를 해결하려면 컬렉션에 저장할 객체 유형을 지정해야 합니다.

 List<Integer> listOfNumbers = new ArrayList<>(); listOfNumbers.add(10); listOfNumbers.add("Twenty"); listOfNumbers.forEach(n -> System.out.println((int) n * 2));

원본과의 유일한 차이점은 컬렉션을 정의하는 라인입니다.

 List<Integer> listOfNumbers = new ArrayList<>();

정수만 저장할 것으로 예상되는 컬렉션에 문자열을 추가하려고 하기 때문에 고정 코드가 컴파일되지 않습니다. 컴파일러는 오류를 표시하고 목록에 "Twenty" 문자열을 추가하려는 라인을 가리킵니다. 제네릭 유형을 매개변수화하는 것은 항상 좋은 생각입니다. 그런 식으로 컴파일러는 가능한 모든 유형 검사를 수행할 수 있으며 유형 시스템 불일치로 인한 런타임 예외 가능성이 최소화됩니다.

결론

플랫폼으로서의 Java는 정교한 JVM과 언어 자체에 의존하여 소프트웨어 개발의 많은 것을 단순화합니다. 그러나 수동 메모리 관리 또는 적절한 OOP 도구를 제거하는 것과 같은 기능이 일반 Java 개발자가 직면하는 모든 문제와 문제를 제거하지는 않습니다. 항상 그렇듯이 지식, 실습 및 이와 같은 Java 자습서는 응용 프로그램 오류를 방지하고 해결하는 가장 좋은 방법입니다. 따라서 라이브러리를 알고, Java를 읽고, JVM 문서를 읽고, 프로그램을 작성하십시오. 정적 코드 분석기는 실제 버그를 지적하고 잠재적인 버그를 강조할 수 있으므로 잊지 마십시오.

관련: 고급 Java 클래스 자습서: 클래스 다시 로드 가이드