JavaRush /Java Blog /Random-KO /FindBugs는 Java를 더 잘 배울 수 있도록 도와줍니다.
articles
레벨 15

FindBugs는 Java를 더 잘 배울 수 있도록 도와줍니다.

Random-KO 그룹에 게시되었습니다
정적 코드 분석기는 부주의로 인해 발생한 오류를 찾는 데 도움이 되기 때문에 널리 사용됩니다. 하지만 훨씬 더 흥미로운 점은 무지로 인해 발생한 실수를 바로잡는 데 도움이 된다는 것입니다. 비록 모든 것이 해당 언어의 공식 문서에 기록되어 있다고 하더라도 모든 프로그래머가 그것을 주의 깊게 읽었다는 것은 사실이 아닙니다. 그리고 프로그래머는 모든 문서를 읽는 데 지치게 될 것이라는 점을 이해할 수 있습니다. 이런 점에서 정적 분석기는 옆에 앉아서 코드 작성을 지켜보는 경험 많은 친구와 같습니다. 그는 "여기가 복사해서 붙여넣을 때 실수한 부분입니다"라고 말할 뿐만 아니라 "아니요, 그렇게 쓸 수는 없습니다. 직접 문서를 살펴보세요"라고 말합니다. 그러한 친구는 문서 자체보다 더 유용합니다. 왜냐하면 그는 작업에서 실제로 접하는 것만 제안하고 결코 유용하지 않을 것에 대해서는 침묵하기 때문입니다. 이 게시물에서는 FindBugs 정적 분석기를 사용하여 배운 Java의 복잡성에 대해 이야기하겠습니다. 아마도 당신에게도 예상치 못한 일이 있을 것입니다. 모든 예제는 추측이 아닌 실제 코드를 기반으로 하는 것이 중요합니다.

삼항 연산자 ?:

삼항 연산자보다 더 간단한 것은 없는 것처럼 보이지만 여기에는 함정이 있습니다. 나는 디자인들 사이에 근본적인 차이가 없다고 믿었고 Type var = condition ? valTrue : valFalse; 여기 Type var; if(condition) var = valTrue; else var = valFalse; 에 미묘함이 있음이 밝혀졌습니다. 삼항 연산자는 복잡한 표현식의 일부일 수 있으므로 해당 결과는 컴파일 타임에 결정되는 구체적인 유형이어야 합니다. 따라서 if 형식의 true 조건을 사용하면 컴파일러는 valTrue를 Type 유형으로 직접 유도하고 삼항 연산자 형식에서는 먼저 공통 유형 valTrue 및 valFalse로 연결합니다(valFalse가 그렇지 않다는 사실에도 불구하고). 평가됨) 결과는 Type 유형으로 이어집니다. 표현식에 기본 유형 및 이에 대한 래퍼(Integer, Double 등)가 포함된 경우 캐스팅 규칙은 완전히 간단하지 않습니다. 모든 규칙은 JLS 15.25에 자세히 설명되어 있습니다. 몇 가지 예를 살펴보겠습니다. Number n = flag ? new Integer(1) : new Double(2.0); 플래그가 설정되면 n은 어떻게 되나요? 값이 1.0인 Double 개체입니다. 컴파일러는 객체를 생성하려는 우리의 서투른 시도를 재미있다고 생각합니다. 두 번째와 세 번째 인수는 서로 다른 기본 유형에 대한 래퍼이므로 컴파일러는 이를 풀어 더 정확한 유형(이 경우 double)을 생성합니다. 그리고 해당 할당에 대한 삼항 연산자를 실행한 후 다시 박싱을 수행합니다. 기본적으로 코드는 다음과 동일합니다. Number n; if( flag ) n = Double.valueOf((double) ( new Integer(1).intValue() )); else n = Double.valueOf(new Double(2.0).doubleValue()); 컴파일러의 관점에서 볼 때 코드에는 문제가 없으며 완벽하게 컴파일됩니다. 그러나 FindBugs에서는 다음과 같은 경고를 표시합니다.
BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR: 기본 값은 TestTernary.main(String[])의 삼항 연산자에 대해 박싱 해제되고 강제됩니다. 래핑된 기본 값은 조건부 삼항 연산자 평가의 일부로 박싱 해제되고 다른 기본 유형으로 변환됩니다(b? e1: e2 연산자). ). Java의 의미 체계에 따르면 e1과 e2가 래핑된 숫자 값인 경우 값은 unboxing되고 공통 유형으로 변환/강제됩니다(예: e1이 Integer 유형이고 e2가 Float 유형인 경우 e1은 unboxing됩니다. 부동 소수점 값으로 변환되어 박스로 표시됩니다. JLS 섹션 15.25를 참조하세요. 물론 FindBugs는 Integer.valueOf(1)가 new Integer(1)보다 더 효율적이라고 경고하지만 모두가 이미 알고 있습니다.
또는 다음 예는 다음과 같습니다. Integer n = flag ? 1 : null; 작성자는 플래그가 설정되지 않은 경우 n에 null을 넣기를 원합니다. 효과가 있을 것 같나요? 예. 하지만 상황을 복잡하게 만들어 보겠습니다. Integer n = flag1 ? 1 : flag2 ? 2 : null; 별 차이가 없는 것 같습니다. 그러나 이제 두 플래그가 모두 지워지면 이 줄에서 NullPointerException이 발생합니다. 오른쪽 삼항 연산자의 옵션은 int 및 null이므로 결과 유형은 Integer입니다. 왼쪽 옵션은 int 및 Integer이므로 Java 규칙에 따라 결과는 int입니다. 이렇게 하려면 예외를 발생시키는 intValue를 호출하여 unboxing을 수행해야 합니다. 코드는 다음과 동일합니다. Integer n; if( flag1 ) n = Integer.valueOf(1); else { if( flag2 ) n = Integer.valueOf(Integer.valueOf(2).intValue()); else n = Integer.valueOf(((Integer)null).intValue()); } 여기서 FindBugs는 오류를 의심하기에 충분한 두 개의 메시지를 생성합니다.
BX_UNBOXING_IMMEDIATELY_REBOXED: 박스화된 값은 박스 해제된 후 TestTernary.main(String[])에서 즉시 리박스 처리됩니다. NP_NULL_ON_SOME_PATH: TestTernary.main(String[])에서 null에 대한 가능한 널 포인터 역참조 실행될 경우 다음을 보장하는 명령문 분기가 있습니다. null 값은 역참조되어 코드가 실행될 때 NullPointerException이 생성됩니다.
이 주제에 대한 마지막 예는 다음과 같습니다. double[] vals = new double[] {1.0, 2.0, 3.0}; double getVal(int idx) { return (idx < 0 || idx >= vals.length) ? null : vals[idx]; } 이 코드가 작동하지 않는 것은 놀라운 일이 아닙니다. 기본 유형을 반환하는 함수가 어떻게 null을 반환할 수 있습니까? 놀랍게도 문제없이 컴파일됩니다. 글쎄, 당신은 이미 그것이 왜 컴파일되는지 이해하고 있습니다.

날짜 형식

Java에서 날짜와 시간의 형식을 지정하려면 DateFormat 인터페이스를 구현하는 클래스를 사용하는 것이 좋습니다. 예를 들어 다음과 같습니다. public String getDate() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } 클래스는 동일한 형식을 반복해서 사용하는 경우가 많습니다. 많은 사람들이 최적화에 대한 아이디어를 생각해 낼 것입니다. 공통 인스턴스를 사용할 수 있는데 왜 매번 형식 객체를 생성합니까? private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public String getDate() { return format.format(new Date()); } 너무 아름답고 멋지지만 안타깝게도 작동하지 않습니다. 보다 정확하게는 작동하지만 때때로 중단됩니다. 사실 DateFormat에 대한 문서에는 다음과 같이 나와 있습니다.
날짜 형식이 동기화되지 않습니다. 각 스레드에 대해 별도의 형식 인스턴스를 만드는 것이 좋습니다. 여러 스레드가 동시에 형식에 액세스하는 경우 외부에서 동기화해야 합니다.
SimpleDateFormat의 내부 구현을 살펴보면 이는 사실입니다. format() 메서드를 실행하는 동안 객체는 클래스 필드에 쓰기 때문에 두 스레드에서 SimpleDateFormat을 동시에 사용하면 어느 정도 확률로 잘못된 결과가 발생할 수 있습니다. FindBugs가 이에 대해 쓴 내용은 다음과 같습니다.
STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE: TestDate.getDate()의 정적 java.text.DateFormat 메소드에 대한 호출 JavaDoc에 따르면 DateFormats는 본질적으로 다중 스레드 사용에 안전하지 않습니다. 탐지기가 정적 필드를 통해 얻은 DateFormat 인스턴스에 대한 호출을 발견했습니다. 의심스러워 보입니다. 이에 대한 자세한 내용은 Sun Bug #6231579 및 Sun Bug #6178997을 참조하십시오.

BigDecimal의 함정

BigDecimal 클래스를 사용하면 임의 정밀도의 분수를 저장할 수 있다는 것을 알게 되고 여기에 double에 대한 생성자가 있다는 것을 알게 되면 일부는 모든 것이 명확하다고 판단하여 다음과 같이 할 수 있습니다. System.out.println(new BigDecimal( 1.1)); 아무도 이 작업을 금지하지 않지만 결과는 예상치 못한 것처럼 보일 수 있습니다: 1.100000000000000088817841970012523233890533447265625. 이는 기본 double이 IEEE754 형식으로 저장되어 1.1을 완벽하게 정확하게 표현하는 것이 불가능하기 때문에 발생합니다(이진수 시스템에서는 무한 주기 분수가 얻어짐). 따라서 1.1에 가장 가까운 값이 여기에 저장됩니다. 반대로 BigDecimal(double) 생성자는 정확하게 작동합니다. IEEE754의 주어진 숫자를 십진수 형식으로 완벽하게 변환합니다(최종 이진 분수는 항상 최종 십진수로 표시 가능). 정확히 1.1을 BigDecimal로 표현하려면 new BigDecimal("1.1") 또는 BigDecimal.valueOf(1.1)을 작성할 수 있습니다. 번호를 바로 표시하지 않고 일부 작업을 수행하면 오류가 어디서 발생하는지 이해하지 못할 수 있습니다. FindBugs는 동일한 조언을 제공하는 경고 DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE을 발행합니다. 또 다른 점은 다음과 같습니다. BigDecimal d1 = new BigDecimal("1.1"); BigDecimal d2 = new BigDecimal("1.10"); System.out.println(d1.equals(d2)); 사실 d1과 d2는 동일한 숫자를 나타내지만, 같음은 숫자 값뿐만 아니라 현재 순서(소수 자릿수)도 비교하기 때문에 false를 반환합니다. 이것은 문서에 기록되어 있지만 Equals와 같은 친숙한 방법에 대한 문서를 읽는 사람은 거의 없습니다. 이러한 문제는 즉시 발생하지 않을 수도 있습니다. 불행히도 FindBugs 자체는 이에 대해 경고하지 않지만 이 버그를 고려한 fb-contrib라는 인기 있는 확장 기능이 있습니다.
MDM_BIGDECIMAL_EQUALS는 두 개의 java.math.BigDecimal 숫자를 비교하기 위해 호출됩니다. 이는 일반적으로 실수입니다. 두 개의 BigDecimal 객체는 값과 소수 자릿수가 모두 동일한 경우에만 동일하므로 2.0은 2.00과 같지 않습니다. BigDecimal 객체의 수학적 동일성을 비교하려면 대신 CompareTo()를 사용하세요.

줄바꿈과 printf

C 이후에 Java로 전환하는 프로그래머는 종종 PrintStream.printf (뿐만 아니라 PrintWriter.printf 등) 를 발견하게 되어 기쁩니다 . 좋습니다. C와 마찬가지로 새로운 것을 배울 필요가 없다는 것을 알고 있습니다. 실제로 차이점이 있습니다. 그 중 하나는 줄 번역에 있습니다. C 언어는 텍스트 스트림과 바이너리 스트림으로 구분됩니다. 어떤 방법으로든 '\n' 문자를 텍스트 스트림으로 출력하면 자동으로 시스템 종속 개행 문자(Windows의 경우 "\r\n")로 변환됩니다. Java에는 이러한 구분이 없습니다. 올바른 문자 순서가 출력 스트림에 전달되어야 합니다. 이는 예를 들어 PrintStream.println 제품군의 메서드를 통해 자동으로 수행됩니다. 그러나 printf를 사용할 때 형식 문자열에 '\n'을 전달하는 것은 시스템에 따른 개행이 아니라 단지 '\n'입니다. 예를 들어, 다음 코드를 작성해 보겠습니다. System.out.printf("%s\n", "str#1"); System.out.println("str#2"); 결과를 파일로 리디렉션하면 다음과 같은 내용을 볼 수 있습니다. FindBugs는 Java를 더 잘 배울 수 있도록 도와줍니다 - 1 따라서 한 스레드에서 줄 바꿈의 이상한 조합을 얻을 수 있는데, 이는 엉성해 보이고 일부 파서의 마음을 놀라게 할 수 있습니다. 특히 Unix 시스템에서 주로 작업하는 경우 오류가 오랫동안 눈에 띄지 않을 수 있습니다. printf를 사용하여 유효한 줄 바꿈을 삽입하려면 특수 서식 문자 "%n"이 사용됩니다. FindBugs가 이에 대해 쓴 내용은 다음과 같습니다.
VA_FORMAT_STRING_USES_NEWLINE: 형식 문자열은 TestNewline.main(String[])에서 \n 대신 %n을 사용해야 합니다. 이 형식 문자열에는 개행 문자(\n)가 포함됩니다. 형식 문자열에서는 일반적으로 플랫폼별 줄 구분 기호를 생성하는 %n을 사용하는 것이 더 좋습니다.
아마도 일부 독자에게는 위의 모든 내용이 오랫동안 알려져 있었을 것입니다. 그러나 나는 그들에게 사용된 프로그래밍 언어의 새로운 기능을 알려주는 정적 분석기의 흥미로운 경고가 있을 것이라고 거의 확신합니다.
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION