JavaRush /Java Blog /Random-KO /커피 브레이크 #146. Java 개발자의 99%가 저지르는 5가지 실수 Java의 문자열 - 내부 보...

커피 브레이크 #146. Java 개발자의 99%가 저지르는 5가지 실수 Java의 문자열 - 내부 보기

Random-KO 그룹에 게시되었습니다

99%의 Java 개발자가 저지르는 5가지 실수

출처: Medium 이 게시물에서는 많은 Java 개발자가 저지르는 가장 일반적인 실수에 대해 알아봅니다. 커피 브레이크 #146.  Java 개발자의 99%가 저지르는 5가지 실수  Java의 문자열 - 내부 보기 - 1Java 프로그래머로서 저는 코드의 버그를 수정하는 데 많은 시간을 소비하는 것이 얼마나 나쁜지 알고 있습니다. 때로는 몇 시간이 걸릴 때도 있습니다. 그러나 개발자가 기본 규칙을 무시하기 때문에 많은 오류가 나타납니다. 즉, 이는 매우 낮은 수준의 오류입니다. 오늘은 몇 가지 일반적인 코딩 실수를 살펴보고 이를 수정하는 방법을 설명하겠습니다. 이것이 귀하의 일상 업무에서 문제를 피하는 데 도움이 되기를 바랍니다.

Objects.equals를 사용하여 객체 비교

나는 당신이 이 방법에 익숙하다고 가정합니다. 많은 개발자들이 자주 사용합니다. JDK 7에 도입된 이 기술은 객체를 빠르게 비교하고 성가신 널 포인터 검사를 효과적으로 방지하는 데 도움이 됩니다. 하지만 이 방법은 가끔 잘못 사용되기도 합니다. 내가 의미하는 바는 다음과 같습니다.
Long longValue = 123L;
System.out.println(longValue==123); //true
System.out.println(Objects.equals(longValue,123)); //false
==를 Objects.equals() 로 바꾸면 왜 잘못된 결과가 나오나요? 이는 == 컴파일러가 longValue 패키징 유형 에 해당하는 기본 데이터 유형을 얻은 다음 이를 기본 데이터 유형과 비교하기 때문입니다. 이는 컴파일러가 상수를 기본 비교 데이터 유형으로 자동 변환하는 것과 동일합니다. Objects.equals() 메서드를 사용한 후 컴파일러 상수의 기본 기본 데이터 유형은 int 입니다 . 다음은 a.equals(b)Long.equals()를 사용 하고 객체의 유형을 결정하는 Objects.equals()의 소스 코드입니다 . 이는 컴파일러가 상수가 int 유형이라고 가정했기 때문에 발생하므로 비교 결과는 false여야 합니다.
public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }

  public boolean equals(Object obj) {
        if (obj instanceof Long) {
            return value == ((Long)obj).longValue();
        }
        return false;
    }
이유를 알면 오류를 수정하는 것은 매우 간단합니다. Objects.equals(longValue,123L) 과 같이 상수의 데이터 유형을 선언하기만 하면 됩니다 . 논리가 엄격하면 위의 문제는 발생하지 않습니다. 우리가 해야 할 일은 명확한 프로그래밍 규칙을 따르는 것입니다.

잘못된 날짜 형식

일상적인 개발에서는 날짜를 변경해야 하는 경우가 많지만, 많은 사람들이 잘못된 형식을 사용하여 예상치 못한 일이 발생합니다. 예는 다음과 같습니다.
Instant instant = Instant.parse("2021-12-31T00:00:00.00Z");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
System.out.println(formatter.format(instant));//2022-12-31 08:00:00
YYYY-MM-dd 형식을 사용하여 날짜를 2021에서 2022로 변경합니다. 그렇게 해서는 안 됩니다. 왜? 이는 Java DateTimeFormatter "YYYY" 패턴이 연도를 매주 목요일로 정의하는 ISO-8601 표준을 기반으로 하기 때문입니다. 하지만 2021년 12월 31일은 금요일이므로 프로그램에서는 2022년을 잘못 표시합니다. 이를 방지하려면 yyyy-MM-dd 형식을 사용하여 날짜 형식을 지정해야 합니다 . 이 오류는 새해가 다가온 후에만 자주 발생합니다. 그런데 우리 회사에서는 생산 실패를 일으켰어요.

ThreadPool에서 ThreadLocal 사용

ThreadLocal 변수를 생성하면 해당 변수에 액세스하는 스레드가 스레드 지역 변수를 생성합니다. 이렇게 하면 스레드 안전 문제를 피할 수 있습니다. 그러나 스레드 풀 에서 ThreadLocal을 사용하는 경우 주의가 필요합니다. 코드에서 예상치 못한 결과가 발생할 수 있습니다. 간단한 예로 전자상거래 플랫폼이 있고 사용자가 제품 구매가 완료되었음을 확인하기 위해 이메일을 보내야 한다고 가정해 보겠습니다.
private ThreadLocal<User> currentUser = ThreadLocal.withInitial(() -> null);

    private ExecutorService executorService = Executors.newFixedThreadPool(4);

    public void executor() {
        executorService.submit(()->{
            User user = currentUser.get();
            Integer userId = user.getId();
            sendEmail(userId);
        });
    }
ThreadLocal을 사용하여 사용자 정보를 저장하면 숨겨진 오류가 나타납니다. 스레드 풀을 사용하고 스레드를 재사용할 수 있기 때문에 ThreadLocal 을 사용하여 사용자 정보를 얻을 때 다른 사람의 정보를 잘못 표시할 수 있습니다. 이 문제를 해결하려면 세션을 사용해야 합니다.

HashSet을 사용하여 중복 데이터 제거

코딩할 때 중복 제거가 필요한 경우가 종종 있습니다. 중복 제거에 대해 생각할 때 많은 사람들이 가장 먼저 생각하는 것은 HashSet 을 사용하는 것입니다 . 그러나 HashSet을 부주의하게 사용하면 중복 제거가 실패할 수 있습니다.
User user1 = new User();
user1.setUsername("test");

User user2 = new User();
user2.setUsername("test");

List<User> users = Arrays.asList(user1, user2);
HashSet<User> sets = new HashSet<>(users);
System.out.println(sets.size());// the size is 2
주의 깊은 독자 중 일부는 실패 이유를 추측할 수 있을 것입니다. HashSet은 해시 코드를 사용하여 해시 테이블에 액세스하고 equals 메서드를 사용하여 객체가 동일한지 확인합니다. 사용자 정의 개체가 hashcode 메서드 및 equals 메서드를 재정의하지 않으면 기본적으로 상위 개체의 hashcode 메서드 및 equals 메서드가 사용됩니다. 이로 인해 HashSet은 두 개체가 서로 다른 개체라고 가정하여 중복 제거가 실패하게 됩니다.

"먹힌" 풀 스레드 제거

ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.submit(()->{
            //do something
            double result = 10/0;
        });
위 코드는 스레드 풀에서 예외가 발생하는 시나리오를 시뮬레이션합니다. 비즈니스 코드는 다양한 상황을 가정해야 하므로 어떤 이유로든 RuntimeException이 발생할 가능성이 매우 높습니다 . 그러나 여기서 특별한 처리가 없으면 이 예외는 스레드 풀에 의해 "먹힐" 것입니다. 그리고 예외의 원인을 확인할 방법도 없습니다. 따라서 프로세스 풀에서 예외를 포착하는 것이 가장 좋습니다.

Java의 문자열 - 내부 보기

출처: Medium 이 기사의 저자는 Java에서 문자열의 생성, 기능 및 특징을 자세히 살펴보기로 결정했습니다. 커피 브레이크 #146.  Java 개발자의 99%가 저지르는 5가지 실수  Java의 문자열 - 내부 보기 - 2

창조

Java의 문자열은 두 가지 방법으로 생성할 수 있습니다. 암시적으로 문자열 리터럴로 생성하는 방법과 명시적으로 new 키워드를 사용하는 방법입니다 . 문자열 리터럴은 큰따옴표로 묶인 문자입니다.
String literal   = "Michael Jordan";
String object    = new String("Michael Jordan");
두 선언 모두 문자열 개체를 생성하지만 이 두 개체가 모두 힙 메모리에 위치하는 방식에는 차이가 있습니다.

내부 표현

이전에는 문자열이 char[] 형식으로 저장되었습니다 . 즉, 각 문자는 문자 배열에서 별도의 요소였습니다. UTF-16 문자 인코딩 형식 으로 표현되었기 때문에 이는 각 문자가 2바이트의 메모리를 차지한다는 것을 의미합니다. 사용 통계에 따르면 대부분의 문자열 개체는 라틴-1 문자로만 구성되어 있으므로 이는 매우 정확하지 않습니다 . Latin-1 문자는 단일 바이트 메모리를 사용하여 표현할 수 있으므로 메모리 사용량을 최대 50%까지 크게 줄일 수 있습니다. Compact Strings라는 JEP 254 기반의 JDK 9 릴리스의 일부로 새로운 내부 문자열 기능이 구현되었습니다 . 이번 릴리스에서는 char[]가 byte[] 로 변경되었으며 사용된 인코딩(Latin-1 또는 UTF-16)을 나타내기 위해 인코더 플래그 필드가 추가되었습니다. 이후에는 문자열 내용을 기반으로 인코딩이 수행됩니다. 값에 Latin-1 문자만 포함된 경우 Latin-1 인코딩( StringLatin1 클래스 )이 사용되거나 UTF-16 인코딩( StringUTF16 클래스 )이 사용됩니다.

메모리 할당

앞에서 설명한 것처럼 힙에서 이러한 개체에 대해 메모리가 할당되는 방식에는 차이가 있습니다. JVM이 힙의 변수에 대한 메모리를 생성하고 할당하므로 명시적 new 키워드를 사용하는 것은 매우 간단합니다. 따라서 문자열 리터럴을 사용하면 인턴이라는 프로세스를 따릅니다. 문자열 인턴은 문자열을 풀에 넣는 프로세스입니다. 이는 변경이 불가능해야 하는 각 개별 문자열 값의 복사본 하나만 저장하는 방법을 사용합니다. 개별 값은 String Intern 풀에 저장됩니다. 이 풀은 리터럴과 해당 해시를 사용하여 생성된 각 문자열 개체에 대한 참조를 저장하는 Hashtable 저장소입니다. 문자열 값은 힙에 있지만 해당 참조는 내부 풀에서 찾을 수 있습니다. 이는 아래의 실험을 통해 쉽게 확인할 수 있습니다. 여기에 동일한 값을 가진 두 개의 변수가 있습니다.
String firstName1   = "Michael";
String firstName2   = "Michael";
System.out.println(firstName1 == firstName2);             //true
코드 실행 중에 JVM이 firstName1 을 발견하면 내부 문자열 풀 Michael 에서 문자열 값을 조회합니다 . 해당 항목을 찾을 수 없으면 내부 풀의 개체에 대한 새 항목이 생성됩니다. 실행이 firstName2 에 도달하면 프로세스가 다시 반복되며 이번에는 firstName1 변수를 기반으로 풀에서 값을 찾을 수 있습니다 . 이렇게 하면 새 항목을 복제하고 생성하는 대신 동일한 링크가 반환됩니다. 그러므로 등식조건이 만족된다. 반면에 new 키워드를 사용하여 Michael 이라는 값을 갖는 변수를 생성하면 인턴이 발생하지 않으며 등식 조건을 만족하지 않습니다.
String firstName3 = new String("Michael");
System.out.println(firstName3 == firstName2);           //false
인턴은 firstName3 intern() 메소드 와 함께 사용할 수 있지만 일반적으로 선호되지는 않습니다.
firstName3 = firstName3.intern();                      //Interning
System.out.println(firstName3 == firstName2);          //true
+ 연산자를 사용하여 두 개의 문자열 리터럴을 연결할 때도 인터닝이 발생할 수 있습니다 .
String fullName = "Michael Jordan";
System.out.println(fullName == "Michael " + "Jordan");     //true
여기서는 컴파일 타임에 컴파일러가 리터럴을 모두 추가하고 표현식에서 + 연산자를 제거하여 아래와 같이 단일 문자열을 형성한다는 것을 알 수 있습니다. 런타임 시 fullName 과 "추가된 리터럴"이 모두 인턴되고 동일 조건이 충족됩니다.
//After Compilation
System.out.println(fullName == "Michael Jordan");

평등

위의 실험에서 기본적으로 문자열 리터럴만 인턴된다는 것을 알 수 있습니다. 그러나 Java 애플리케이션은 다양한 소스에서 문자열을 받을 수 있으므로 확실히 문자열 리터럴만 갖지는 않습니다. 따라서 같음 연산자를 사용하는 것은 권장되지 않으며 바람직하지 않은 결과가 발생할 수 있습니다. 동등성 테스트는 equals 메소드 로만 수행되어야 합니다 . 문자열이 저장된 메모리 주소가 아닌 문자열 값을 기준으로 동일성을 수행합니다.
System.out.println(firstName1.equals(firstName2));       //true
System.out.println(firstName3.equals(firstName2));       //true
equalsIgnoreCase 라고 하는 약간 수정된 버전의 equals 메소드도 있습니다 . 대소문자를 구분하지 않는 경우 유용할 수 있습니다.
String firstName4 = "miCHAEL";
System.out.println(firstName4.equalsIgnoreCase(firstName1));  //true

불변성

문자열은 불변입니다. 즉, 일단 생성되면 내부 상태를 변경할 수 없습니다. 변수의 값은 변경할 수 있지만 문자열 자체의 값은 변경할 수 없습니다. 객체 조작(예: concat , substring )을 처리하는 String 클래스 의 각 메서드는 기존 값을 업데이트하는 대신 값의 새 복사본을 반환합니다.
String firstName  = "Michael";
String lastName   = "Jordan";
firstName.concat(lastName);

System.out.println(firstName);                       //Michael
System.out.println(lastName);                        //Jordan
보시다시피 firstName 또는 lastName 변수에는 변경 사항이 발생하지 않습니다 . String 클래스 메서드는 내부 상태를 변경하지 않으며 결과의 새 복사본을 만들고 아래와 같이 결과를 반환합니다.
firstName = firstName.concat(lastName);

System.out.println(firstName);                      //MichaelJordan
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION