JavaRush /Java Blog /Random-KO /사물의 비교: 연습
articles
레벨 15

사물의 비교: 연습

Random-KO 그룹에 게시되었습니다
이것은 객체 비교에 관한 기사 중 두 번째입니다. 첫 번째는 비교의 이론적 기초, 즉 비교가 수행되는 방법, 이유 및 사용 위치에 대해 논의했습니다. 이 기사에서는 숫자, 대상, 특별한 경우, 미묘함 및 명확하지 않은 점을 비교하는 방법에 대해 직접 설명합니다. 보다 정확하게는 다음과 같이 이야기하겠습니다.
개체 비교: 연습 - 1
  • 문자열 비교: ' ==' 및equals
  • 방법String.intern
  • 실제 프리미티브 비교
  • +0.0그리고-0.0
  • 의미NaN
  • 자바 5.0. ==' ' 를 통한 메소드 생성 및 비교
  • 자바 5.0. Autoboxing/Unboxing: 객체 래퍼의 경우 ' ==', ' >=' 및 ' <='.
  • 자바 5.0. 열거형 요소 비교(유형 enum)
그럼 시작해 보겠습니다!

문자열 비교: ' ==' 및equals

아, 이 줄은... 가장 일반적으로 사용되는 유형 중 하나이므로 많은 문제가 발생합니다. 원칙적으로 이에 대한 별도의 기사가 있습니다 . 여기서는 비교 문제를 다루겠습니다. 물론 문자열은 를 사용하여 비교할 수 있습니다 equals. 또한 를 통해 비교해야 합니다 equals. 그러나 알아야 할 가치가 있는 미묘한 부분이 있습니다. 우선, 동일한 문자열은 실제로 단일 객체입니다. 이는 다음 코드를 실행하여 쉽게 확인할 수 있습니다.
String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
결과는 "동일" 입니다 . 이는 문자열 참조가 동일함을 의미합니다. 이는 분명히 메모리를 절약하기 위해 컴파일러 수준에서 수행됩니다. 컴파일러는 문자열의 인스턴스 하나를 생성하고 이 인스턴스에 대한 참조를 할당 str1합니다 str2. 그러나 이는 코드에서 리터럴로 선언된 문자열에만 적용됩니다. 조각으로 문자열을 구성하면 해당 링크에 대한 링크가 달라집니다. 확인 - 이 예:
String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
결과는 "같지 않음"이 됩니다 . 복사 생성자를 사용하여 새 객체를 만들 수도 있습니다.
String str1 = "string";
String str2 = new String("string");
System.out.println(str1==str2 ? "the same" : "not the same");
결과도 "같지 않음"이 됩니다 . 따라서 때로는 참조 비교를 통해 문자열을 비교할 수 있습니다. 그러나 이것에 의존하지 않는 것이 좋습니다. 나는 소위 문자열의 표준 표현을 얻을 수 있는 매우 흥미로운 방법 중 하나를 다루고 싶습니다 String.intern. 그것에 대해 더 자세히 이야기합시다.

String.intern 메서드

String클래스가 문자열 풀을 지원한다는 사실부터 시작해 보겠습니다 . 클래스뿐만 아니라 클래스에 정의된 모든 문자열 리터럴이 이 풀에 추가됩니다. 따라서 이 메서드를 사용하면 의 관점에서 intern기존 풀(메서드가 호출되는 풀)과 동일한 문자열을 이 풀에서 가져올 수 있습니다 . 해당 행이 풀에 존재하지 않으면 기존 행이 풀에 배치되고 해당 행에 대한 링크가 반환됩니다. 따라서 두 개의 동일한 문자열에 대한 참조가 다르더라도(위의 두 예에서처럼) 이러한 문자열을 호출하면 동일한 객체에 대한 참조가 반환됩니다. internequalsintern
String str1 = "string";
String str2 = new String("string");
System.out.println(str1.intern()==str2.intern() ? "the same" : "not the same");
이 코드 조각을 실행한 결과는 "동일" 합니다 . 왜 이런 식으로 처리되었는지 정확히 말할 수는 없습니다. 이 방법은 intern기본적이며 솔직히 말해서 저는 C 코드의 세계에 들어가고 싶지 않습니다. 대부분 이는 메모리 소비와 성능을 최적화하기 위해 수행됩니다. 어쨌든 이 구현 기능에 대해 아는 것은 가치가 있습니다. 다음 부분으로 넘어 갑시다.

실제 프리미티브 비교

우선 질문을 하고 싶습니다. 매우 간단합니다. 다음 합은 무엇입니까 - 0.3f + 0.4f? 왜? 0.7f? 점검 해보자:
float f1 = 0.7f;
float f2 = 0.3f + 0.4f;
System.out.println("f1==f2: "+(f1==f2));
결과적으로? 좋다? 저도요. 이 부분을 완성하지 못한 분들을 위해 말씀드리자면, 그 결과는...
f1==f2: false
왜 이런 일이 발생하는 걸까요?.. 또 다른 테스트를 수행해 보겠습니다.
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("f1="+(double)f1);
System.out.println("f2="+(double)f2);
System.out.println("f3="+(double)f3);
System.out.println("f4="+(double)f4);
로의 변환을 참고하세요 double. 이는 더 많은 소수 자릿수를 출력하기 위해 수행됩니다. 결과:
f1=0.30000001192092896
f2=0.4000000059604645
f3=0.7000000476837158
f4=0.699999988079071
엄밀히 말하면 결과는 예측 가능하다. 분수 부분의 표현은 유한 계열 2-n을 사용하여 수행되므로 임의로 선택한 숫자의 정확한 표현에 대해 말할 필요가 없습니다. 예시에서 볼 수 있듯이 표현 정확도는 float소수점 이하 7자리입니다. 엄밀히 말하면 표현은 float 가수에 24비트를 할당합니다. float 따라서 (정확도에 대해 이야기하고 있기 때문에 정도를 고려하지 않고)를 사용하여 표현할 수 있는 최소 절대 숫자는 2-24≒6*10-8입니다. 표현의 값이 실제로 이동하는 것은 이 단계입니다 float. 그리고 양자화가 있기 때문에 오류도 있습니다. 따라서 결론은 다음과 같습니다. 표현의 숫자는 float특정 정확도로만 비교할 수 있습니다. 소수점 이하 6번째 자리(10-6)로 반올림하거나, 바람직하게는 둘 사이의 차이의 절대값을 확인하는 것이 좋습니다 .
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("|f3-f4|<1e-6: "+( Math.abs(f3-f4) < 1e-6 ));
이 경우 결과는 고무적입니다.
|f3-f4|<1e-6: true
물론, 그림은 유형과 정확히 동일합니다 double. 유일한 차이점은 가수에 53비트가 할당되므로 표현 정확도는 2-53≒10-16입니다. 예, 양자화 값은 훨씬 작지만 존재합니다. 그리고 잔인한 농담을 할 수도 있습니다. 그런데 JUnit 테스트 라이브러리에서는 실수 비교 방법에서 정밀도가 명시적으로 지정됩니다. 저것들. 비교 방법에는 세 가지 매개변수, 즉 숫자, 동일해야 하는 값, 비교 정확도가 포함됩니다. 그건 그렇고, 나는 정도를 나타내는 과학적인 형식으로 숫자를 쓰는 것과 관련된 미묘함을 언급하고 싶습니다. 질문. 10-6은 어떻게 작성하나요? 실습에 따르면 80% 이상의 답변이 10e-6입니다. 그 사이 정답은 1e-6 입니다! 그리고 10e-6은 10-5입니다! 우리는 프로젝트 중 하나에서 이 갈퀴를 아주 예상치 못하게 밟았습니다. 그들은 매우 오랜 시간 동안 오류를 찾고 상수를 20번 보았습니다. 어느 날 우연히 상수 10e-3이 인쇄되어 두 가지를 발견할 때까지 아무도 그들의 정확성에 대해 의심의 여지가 없었습니다. 예상되는 3자리 대신 소수점 이하 자릿수. 그러므로 조심하세요! 계속 진행합시다.

+0.0 및 -0.0

실수 표현에서는 최상위 비트가 부호가 있습니다. 다른 모든 비트가 0이면 어떻게 되나요? 이러한 상황에서 결과가 표현 범위의 하한에 위치한 음수인 정수와 달리, 최상위 비트만 1로 설정된 실수도 0을 의미하며 마이너스 기호만 있습니다. 따라서 +0.0과 -0.0이라는 두 개의 0이 있습니다. 논리적인 질문이 생깁니다. 이 숫자를 동일한 것으로 간주해야 합니까? 가상머신은 정확히 이런 식으로 생각합니다. 그러나 이들은 서로 다른 두 개의 숫자입니다. 왜냐하면 이들로 작업한 결과 서로 다른 값이 얻어지기 때문입니다.
float f1 = 0.0f/1.0f;
float f2 = 0.0f/-1.0f;
System.out.println("f1="+f1);
System.out.println("f2="+f2);
System.out.println("f1==f2: "+(f1==f2));
float f3 = 1.0f / f1;
float f4 = 1.0f / f2;
System.out.println("f3="+f3);
System.out.println("f4="+f4);
... 결과는 다음과 같습니다.
f1=0.0
f2=-0.0
f1==f2: true
f3=Infinity
f4=-Infinity
따라서 어떤 경우에는 +0.0과 -0.0을 두 개의 다른 숫자로 처리하는 것이 합리적입니다. 그리고 필드가 +0.0이고 다른 하나는 -0.0인 두 개의 개체가 있는 경우 이러한 개체도 동일하지 않은 것으로 간주될 수 있습니다. 문제가 발생합니다. 가상 머신과 직접 비교하면 숫자가 동일하지 않다는 것을 어떻게 이해할 수 있습니까 true? 대답은 이것입니다. 가상 머신이 이러한 숫자를 동일한 것으로 간주하더라도 해당 표현은 여전히 ​​다릅니다. 그러므로 할 수 있는 유일한 일은 견해를 비교하는 것입니다. 그리고 이를 얻기 위해 각각 및 형식으로 비트 표현을 반환하는 메서드 int Float.floatToIntBits(float)및 가 있습니다 (이전 예제에 이어). long Double.doubleToLongBits(double)intlong
int i1 = Float.floatToIntBits(f1);
int i2 = Float.floatToIntBits(f2);
System.out.println("i1 (+0.0):"+ Integer.toBinaryString(i1));
System.out.println("i2 (-0.0):"+ Integer.toBinaryString(i2));
System.out.println("i1==i2: "+(i1 == i2));
결과는 다음과 같습니다
i1 (+0.0):0
i2 (-0.0):10000000000000000000000000000000
i1==i2: false
따라서 +0.0과 -0.0이 다른 숫자인 경우 비트 표현을 통해 실제 변수를 비교해야 합니다. +0.0과 -0.0을 정리한 것 같습니다. 그러나 -0.0이 유일한 놀라움은 아닙니다. 이런것도 있는데...

NaN 값

NaN을 의미합니다 Not-a-Number. 이 값은 0.0을 0.0으로 나누기, 무한대를 무한대로 나누는 등 잘못된 수학 연산의 결과로 나타납니다. 이 값의 특징은 그 자체와 동일하지 않다는 것입니다. 저것들.:
float x = 0.0f/0.0f;
System.out.println("x="+x);
System.out.println("x==x: "+(x==x));
...결과가 나올 것이다...
x=NaN
x==x: false
물체를 비교할 때 이것이 어떻게 나타날 수 있습니까? 객체의 필드가 와 같으면 NaN비교 결과는 가 됩니다 false. 즉, 객체는 불평등한 것으로 간주됩니다. 그러나 논리적으로 우리는 그 반대를 원할 수도 있습니다. 메소드를 사용하여 원하는 결과를 얻을 수 있습니다 Float.isNaN(float). true인수가 이면 반환됩니다 NaN. 이 경우 비트 표현 비교에 의존하지 않을 것입니다. 표준화되지 않았습니다. 아마도 프리미티브에 대해서는 이것으로 충분할 것입니다. 이제 버전 5.0 이후 Java에 나타난 미묘한 부분으로 넘어가겠습니다. 그리고 제가 가장 먼저 언급하고 싶은 점은

자바 5.0. ==' ' 를 통한 메소드 생성 및 비교

디자인에는 제작방식 이라는 패턴이 있다. 때로는 생성자를 사용하는 것보다 그 사용이 훨씬 더 수익성이 높습니다. 예를 들어 보겠습니다. 나는 객체쉘에 대해 잘 알고 있는 것 같다 Boolean. 이 클래스는 변경할 수 없으며 두 개의 값만 포함할 수 있습니다. 즉, 실제로 어떤 요구 사항에도 두 개의 사본만으로 충분합니다. 그리고 미리 생성한 다음 간단히 반환하면 생성자를 사용하는 것보다 훨씬 빠릅니다. 다음 과 같은 방법이 있습니다 Boolean. valueOf(boolean)버전 1.4에서 등장했습니다. 유사한 생성 방법이 버전 5.0의 Byte, Character, 및 클래스 에 도입되었습니다 . 이러한 클래스가 로드되면 특정 범위의 기본 값에 해당하는 인스턴스 배열이 생성됩니다. 이러한 범위는 다음과 같습니다. ShortIntegerLong
개체 비교: 연습 - 2
즉, 메서드를 사용할 때 valueOf(...)인수가 지정된 범위 내에 있으면 항상 동일한 개체가 반환됩니다. 아마도 이것은 속도를 약간 증가시킬 것입니다. 그러나 동시에 문제의 원인을 파악하기가 상당히 어려울 수 있는 성격의 문제가 발생합니다. 그것에 대해 자세히 읽어보십시오. 이론적으로는 및 클래스 모두에 생성 방법이 valueOf추가되었습니다 . 설명에 따르면 새 복사본이 필요하지 않으면 이 방법을 사용하는 것이 더 좋습니다. 속도 등이 향상될 수 있습니다. 등등. 그러나 현재(Java 5.0) 구현에서는 이 메소드에서 새 인스턴스가 생성됩니다. 그 사용은 속도 증가를 보장하지 않습니다. 게다가 값의 연속성으로 인해 캐시를 구성할 수 없기 때문에 이 방법을 어떻게 가속화할 수 있는지 상상하기 어렵습니다. 정수는 제외됩니다. 내 말은, 분수 부분이 없다는 거죠.FloatDouble

자바 5.0. Autoboxing/Unboxing: 객체 래퍼의 경우 ' ==', ' >=' 및 ' <='.

작업을 최적화하기 위해 정수 프리미티브용 래퍼에 생산 방법과 인스턴스 캐시가 추가된 것 같습니다 autoboxing/unboxing. 그것이 무엇인지 상기시켜 드리겠습니다. 객체가 작업에 포함되어야 하지만 기본 요소가 포함된 경우 이 기본 요소는 자동으로 개체 래퍼에 래핑됩니다. 이것 autoboxing. 그 반대의 경우도 마찬가지입니다. 기본 요소가 작업에 포함되어야 하는 경우 거기에서 개체 셸을 대체할 수 있으며 값은 자동으로 확장됩니다. 이것 unboxing. 당연히 그러한 편리함을 위해서는 비용을 지불해야 합니다. 자동 변환 작업은 애플리케이션 성능을 다소 저하시킵니다. 그러나 이는 현재 주제와는 관련이 없으므로 이 질문을 남겨 두겠습니다. 기본 요소나 쉘과 명확하게 관련된 작업을 처리하는 한 모든 것이 괜찮습니다. ' ' 작업은 어떻게 되나요 ==? Integer내부에 동일한 값을 가진 두 개의 객체가 있다고 가정해 보겠습니다 . 그들은 어떻게 비교될 것인가?
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1==i2: "+(i1==i2));
결과:
i1==i2: false

Кто бы сомневался... Сравниваются они How an objectы. А если так:Integer i1 = 1;
Integer i2 = 1;
System.out.println("i1==i2: "+(i1==i2));
결과:
i1==i2: true
이제 이것이 더 흥미로워졌습니다! -e 인 경우 autoboxing동일한 객체가 반환됩니다! 여기에 함정이 있습니다. 동일한 객체가 반환된다는 사실을 발견하면 이것이 항상 사실인지 확인하기 위해 실험을 시작할 것입니다. 그리고 얼마나 많은 값을 확인할 것인가? 하나? 십? 100? 아마도 우리는 0을 중심으로 각 방향에서 100으로 제한할 것입니다. 그리고 우리는 어디에서나 평등을 누리고 있습니다. 모든 것이 괜찮은 것 같습니다. 그러나 조금 되돌아 보면 여기 . 캐치가 무엇인지 짐작하셨나요?.. 예, 오토박싱 중 개체 쉘의 인스턴스는 생성 방법을 사용하여 생성됩니다. 이는 다음 테스트에서 잘 설명됩니다.
public class AutoboxingTest {

    private static final int numbers[] = new int[]{-129,-128,127,128};

    public static void main(String[] args) {
        for (int number : numbers) {
            Integer i1 = number;
            Integer i2 = number;
            System.out.println("number=" + number + ": " + (i1 == i2));
        }
    }
}
결과는 다음과 같습니다:
number=-129: false
number=-128: true
number=127: true
number=128: false
캐싱 범위 내에 속하는 값의 경우 동일한 개체가 반환되고, 그 범위를 벗어나는 경우에는 다른 개체가 반환됩니다. 따라서 기본 요소 대신 응용 프로그램 셸의 어딘가를 비교하면 가장 끔찍한 오류인 부동 오류가 발생할 가능성이 있습니다. 코드는 이 오류가 나타나지 않는 제한된 범위의 값에 대해서도 테스트될 가능성이 높기 때문입니다. 그러나 실제 작업에서는 일부 계산 결과에 따라 나타나기도 하고 사라지기도 합니다. 그런 실수를 찾는 것보다 미쳐가는 것이 더 쉽습니다. 따라서 가능하면 오토박싱을 피하는 것이 좋습니다. 그리고 그게 다가 아닙니다. 5 학년 이하의 수학을 기억합시다. 불평등 A>=BА<=B. A과의 관계에 대해 무엇을 말할 수 있습니까 B? 단 한 가지만 있습니다. 그들은 동일합니다. 동의하시나요? 맞는 것 같아요. 테스트를 실행해 보겠습니다.
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1>=i2: "+(i1>=i2));
System.out.println("i1<=i2: "+(i1<=i2));
System.out.println("i1==i2: "+(i1==i2));
결과:
i1>=i2: true
i1<=i2: true
i1==i2: false
그리고 이것은 나에게 가장 이상한 것입니다. 그러한 모순이 발생한다면 왜 이 기능이 언어에 도입되었는지 전혀 이해가 되지 않습니다. 일반적으로 다시 한 번 반복하겠습니다. 없이 할 수 있다면 autoboxing/unboxing이 기회를 최대한 활용하는 것이 좋습니다. 제가 다루고 싶은 마지막 주제는 Java 5.0입니다. 열거형 요소 비교(열거형) 아시다시피, 버전 5.0부터 Java에서는 열거 형과 같은 유형을 도입했습니다. 해당 인스턴스에는 기본적으로 클래스의 인스턴스 선언에 있는 이름과 시퀀스 번호가 포함되어 있습니다. 따라서 발표 순서가 변경되면 숫자도 변경됩니다. 그러나 '그대로 직렬화' 라는 글에서 말했듯 이, 이는 문제를 일으키지 않습니다. 모든 열거 요소는 단일 복사본에 존재하며 이는 가상 머신 수준에서 제어됩니다. 따라서 링크를 이용하여 직접 비교할 수 있습니다. * * * 아마도 오늘은 객체 비교 구현의 실제적인 측면에 대한 내용이 전부일 것입니다. 아마도 내가 뭔가를 놓쳤을 것입니다. 언제나 그렇듯, 여러분의 의견을 기다리겠습니다! 일단은 이만 가보겠습니다. 관심을 가져주셔서 감사합니다! 출처 링크: 객체 비교: 연습
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION