자바(Java)는 강력하고 널리 사용되는 프로그래밍 언어지만, 몇 가지 단점도 존재합니다. 아래는 자바의 주요 단점을 정리한 내용입니다:
자바는 JVM(Java Virtual Machine)에서 동작하는 인터프리터 기반 언어로, C/C++ 같은 네이티브 언어에 비해 성능이 느릴 수 있습니다.
가비지 컬렉션(Garbage Collection)이 실행되는 동안 애플리케이션이 멈출 가능성(Stop-the-world)이 있어 성능 저하를 유발할 수 있습니다.
자바는 객체 지향 언어로, 객체 생성이 빈번하게 일어나며 이로 인해 메모리 사용량이 많아질 수 있습니다.
자동 메모리 관리(Garbage Collection)가 장점이지만, 비효율적으로 작동할 경우 메모리 누수 문제가 발생할 수 있습니다.
자바는 장황한(Syntax-heavy) 코드 스타일을 가지고 있어 간단한 작업도 비교적 긴 코드로 작성해야 하는 경우가 많습니다.
예를 들어, 람다 표현식 도입 전에는 익명 클래스와 같은 복잡한 문법이 요구되었습니다.
자바는 "Write Once, Run Anywhere(WORA)"를 표방하지만, 플랫폼 및 JVM 버전에 따라 예상치 못한 호환성 문제가 발생할 수 있습니다.
특정 운영 체제나 환경에서 JVM 설정을 세부적으로 조정해야 할 때가 있습니다.
자바 애플리케이션은 JVM이 필요하기 때문에 실행 환경에서 추가적인 설치가 필요합니다.
JAR(Java Archive) 파일 및 라이브러리가 포함된 프로젝트는 크기가 커질 수 있습니다.
PC에 JDK가 설치되었다면 자바 언어로 작성한 소스 파일을 만들고 컴파일을 할 수 있다.
자바 소스파일 확장명은 .java이다.

소스파일(.java)은 javac(java compiler) 실행파일에 의해 컴파일되어 바이트코드(.class)파일이 된다. 바이트코드 파일을 특정 운영체제가 이해하는 기계어로 번역하고 실행시키는 명령어는 java이다. java 명령어는 JDK와 함께 설치된 자바 가상 머신을 구동시켜 바이트코드 파일을 완전한 기계어로 번역하고 실행시킨다.

바이트코드(Bytecode) 파일은 운영체제와 상관없이 모두 동일한 내용으로 생성되지만, 자바 가상 머신(JVM, Java Virtual Machine)은 운영체제에서 이해하는 기계어로 번역해야 하므로 운영체제별로 다르게 설치된다. 그래서 운영체제별로 설치하는 JDK가 다른 것이다.

자바 바이트 코드를 실행시키기 위한 가상의 기계인 자바 가상 머신은 다음과 같은 구성을 가지고 있다.
자바 컴파일러에 의해 변환된 자바 바이트 코드를 읽고 해석하는 역할을 함
자바는 동적으로 클래스를 읽어오므로, 프로그램이 실행 중인 런타임에서야 모든 코드가 자바 가상 머신과 연결됨. 이렇게 동적으로 클래스를 로딩해주는 역할을 하는 것이 바로 클래스 로더임
자바 컬파일러가 프로그램이 실행 중인 런타임에 자바 컴파일러가 생성한 자바 바이트 코드를 기계어로 변환하는데 사용하는 것임.
동적 번역(dynamic translation)이라고도 불리는 이 기법은 프로그램의 실행 속도를 향상시키기 위해 개발됨
자바의 메모리 관리 방법 중의 하나로 JVM(자바 가상 머신)의 Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 메모리 객체(garbage)를 모아 주기적으로 제거하는 프로세스를 말함
참고 : Garbage Collector란? - 정리 예정


“정확히는 JIT 컴파일러가 ‘번역 안 할래’가 아니라, 이미 자주 실행되는 부분을 미리 기계어로 컴파일해두므로 인터프리터가 해당 부분을 굳이 한 줄씩 해석하지 않아도 된다는 뜻이다.”
2014년 출시, LTS 버전(~2030.12 지원)
대규모 릴리즈, Lambda, Stream API 제공
Optional, 새로운 날짜,시간 API 제공 (ex: LocalDateTime)
Oracle이 Java를 인수한 후 첫번째 LTS 출시 버전
2018년 출시, LTS 버전(~2032.01 지원)
String과 File 기능 향상
String: isBlank(), strip() File: writeString(), readString()
var 키워드 사용 가능
Open JDK와 Oracle JDK가 통합
G1 GC가 기본 GC로 설정
2021년 출시, LTS 버전(~2029.09 지원)
Spring Boot 3.x.x 버전은 JDK 17 이상 부터 지원
가상 스레드: Java 플랫폼에 경량 가상 스레드를 도입
가상 스레드의 도입으로 몇 개의 운영 체제 스레드만 사용하여 수백만 개의 가상 스레드를 실행하는 것이 가능해짐. 기존 Java 코드를 수정할 필요 X
UTF-8이 기본값으로 사용
JRE란 번역하면 자바 실행환경으로 자바 프로그램을 실행하는데 필요한 것이다.
즉, 자바 프로그램을 실행시키는데는 문제가 없지만 자바 프로그램을 코딩할 때 jdk가 아니라 jre를 사용하면 문제점이 생길 수 있다.
예를 들어 컴파일이 정상적으로 되지 않을 수도 있다.
JDK(Java Development Kit)란?
번역하면 자바 개발 키트이다. 간단하게 설명하면 자바를 개발하는데 필요한 기능들이 들어간 것이다.
여기에는 물론 자바를 실행하는데 필요한 jre도 포함되어 있어서 jdk를 다운로드 받으면 jre 또한 포함되어 있다.
자바 프로그램 개발을 위해서는 바로 이 jdk를 다운로드 받아 자바 기능을 사용하고 컴파일 해야하는 것이다.
동일성은 두 객체가 같은 메모리 주소값을 가지고 있다는 것을 의미합니다.
동등성은 두 객체가 논리적으로 같은 내용(값)을 가지고 있다는 것을 의미합니다.
두 객체가 동일하다면 당연히 같은 주소를 참조하므로 동등한 내용(값, 동등성)을 가지지만, 두 객체가 동등하다면 같은 내용(값)을 가지고 있다 하더라도 메모리 주소는 다를 수 있기 때문에 동일하지 않을 수 있습니다.
int와 boolean과 같은 Primitive Type의 비교는 ==이라는 연산자를 사용하여 비교한다. '==' 연산자는 객체의 메모리 주소를 비교하기 때문에 두 객체의 참조가 같은지, 즉 동일한 객체를 가리키고 있는지를 확인한다.
하지만 String처럼 Reference Type의 값을 비교할때는 == 대신 equals()라는 메소드를 사용해 비교한다. 동등성의 비교를 위해서는 'equals()' 메소드를 사용해야하기 때문이다. 왜냐하면 'equals()' 메소드는 객체의 내용이나 상태를 기반으로 두 객체가 같은지를 판단하기 때문이다.
(다만 equals()가 재정의되지 않은 Object 등은 동등성 비교가 되지 않고, equals()의 기본 작동 방식인 참조 비교를 할 수 있음. String은 재정의가 되어 있기 때문에 동등성 비교 가능)
참고 : 자바 문자열 비교 == equals() 차이점
정의
자바의 Object 클래스가 제공하는 hashCode() 메서드는 객체를 구별하기 위한 정수 해시 값(hash code)을 반환한다.
용도
주로 HashMap, HashSet, Hashtable 등 해시 기반 자료구조에서 객체를 빠르게 조회하거나 저장할 때 사용한다.
객체가 저장될 위치(버킷)를 결정하는 데 활용되므로, 같은 객체(동등한 객체)는 반드시 동일한 해시 코드를 가져야 데이터가 정상적으로 관리된다.
특징
equals()가 true를 반환한다면) 반드시 동일한 해시 코드를 반환하도록 구현해야 한다. equals() 메서드
Object의 기본 equals()는 “동일한 참조(주소)인가?”만 확인(즉, == 동작과 유사) hashCode() 메서드
equals()를 재정의할 때, 반드시 해시 기반 자료구조에 사용할 수 있도록 hashCode()도 같이 재정의해야 함왜 함께 재정의해야 하는가?
equals()에서 “동등”이라 판별된 객체는 동일한 hashCode()를 가져야, HashMap/HashSet 등에서 정상적으로 동작함 equals(), hashCode(), toString() 등의 기본 메서드를 제공한다.equals()와 논리적으로 일관되어야 한다.equals()와 hashCode()는 객체 동등성 관리와 해시 기반 자료구조의 올바른 동작을 위해 함께 재정의한다. 이처럼 equals()와 hashCode()를 어떻게 정의하느냐에 따라, 객체를 어떻게 “같다”고 판단하고 어떻게 해시 자료구조에 저장할지가 결정된다. 이는 자바 프로그래밍에서 객체의 신뢰성 있는 비교와 효율적 자료구조 활용에 필수적이다.
참고 : 자바의 Object 클래스와 equals, hashCode 메서드의 중요성

toString() 메소드는 해당 인스턴스에 대한 정보를 문자열로 반환한다. 이 메서드는 인스턴스에 대한 정보를 문자열로 제공할 목적으로 정의되어 있는 것이다.
이때 반환되는 문자열은 클래스 이름과 함께 구분자로 @가 사용되며, 그 뒤로 16진수 해시 코드(hash code)가 추가된다. 해시 코드 값은 인스턴스의 주소를 해싱하여 변환한 값으로, 고유 숫자로서 인스턴스마다 모두 다르게 반환된다.
실제로 toString() 메서드 내부를 본다면 다음과 같이 구현되어있다.
하지만 만일 객체의 이름이나 주소값이 아닌 객체의 고유 정보를 출력하고 싶을 때가 있다.
예를 들어 다음 Person 객체를 출력하면 원론적인 값이 아닌, Person의 이름이나 나이 같은 고유 정보를 출력하고 싶을 때 바로 오버라이딩(Overriding)을 통해 toString() 메소드를 재정의 해주면 된다.
메서드 오버라이딩을 할때 바로 뒤에서 배울 접근제어자 지정에 대해 조심해야 할 점이 있다.바로 부모 메서드에 정의된 접근제어자의 범위보다 낮은 범위의 제어자를 지정할수 없다는 것인데, Object 클래스의 toString 메서드의 접근제어자는 protected void toString() 라고 정의되어있어서, 이것을 오버라이딩 했을때 같은 protected 나 public 으로 설정해야 오버라이딩이 된다. 만일 private 나 default 제어자로 오버라이딩을 하려고 하면 컴파일 에러가 생긴다.
예를들어 배열에 다음 MyInt 라는 객체 자료를 저장해서 출력하고 싶다고 하자.
MyInt 클래스는 정수를 인자로 받아 100을 곱한 수를 저장하는 특별한 객체 자료형이다. 이를 배열에 적재하고 배열을 출력해보자.
import java.util.Arrays;
class MyInt {
final int num;
MyInt(int num) {
this.num = num * 100;
}
}
public class Main {
public static void main(String[] args) {
Object[] arr = new Object[5];
arr[0] = new MyInt(0);
arr[1] = new MyInt(1);
arr[2] = new MyInt(2);
arr[3] = new MyInt(3);
arr[4] = new MyInt(4);
System.out.println(Arrays.toString(arr));
}
}

우리는 [100, 200, 300, 400, 500] 으로 출력되기를 기대했지만, 엉뚱하게 객체 정보값이 출력되어 버렸다.
배열을 스트링으로 출력하기 위해 Arrays.toString() 메서드를 사용했어도 말이다.
이런식으로 출력된 이유는 Arrays.toString() 메서드는 배열 자체를 스트링화 한것이고, 배열 내의 각각의 원소들은 스트링화 하지 않기 때문에 발생한 현상이다.
따라서 배열 요소들도 정수값으로 출력되게 하고 싶다면, 그 객체 요소의 클래스 정의문에 toString() 을 재정의해야 한다.
즉, 핵심은 배열의 원소들이 int형 같은 primitive 타입이 아닌 객체와 같은 reference 타입일 경우, 배열을 출력할때 안에 있는 값을 출력하기 위해선 반드시 일일히 각 객체의 클래스마다 toString() 을 재정의해야 된다는 소리이다.
import java.util.Arrays;
class MyInt {
final int num;
MyInt(int num) {
this.num = num * 100;
}
@Override
public String toString() {
return Integer.toString(num);
}
}
public class Main {
public static void main(String[] args) {
Object[] arr = new Object[5];
arr[0] = new MyInt(1);
arr[1] = new MyInt(2);
arr[2] = new MyInt(3);
arr[3] = new MyInt(4);
arr[4] = new MyInt(5);
System.out.println(Arrays.toString(arr));
}
}

반면, Integer 클래스에서는 toString 메서드가 미리 오버라이딩 되어 있기 때문에 별다른 작업없이 배열을 출력하면 Wrapper 객체가 문자열로 변환된 것이다.
참고 : 자바 toString 오버라이딩 - 완벽 이해하기
위에서 이야기한 Wrapper(Primitive type을 객체 형태로 다룰 수 있게 하는 클래스)는 Reference Type에 속한다. 그럼 과연 Primitive와 Reference의 차이가 무엇일까?
우선 둘은 데이터타입의 유형을 의미한다.
int, long, double, float, boolean, byte, short, char
반드시 사용하기 전에 선언되어야하며, 자료형의 길이는 운영체제에 독립적이며 변하지 않는다. stack 메모리에 저장된다.
Integer, Long, Double, Float, Boolean, Byte, Short, Char, 배열, 열거, 클래스, 인터페이스
즉, Java에서 최상위 클래스인 java.lang.Object 클래스를 상속하는 모든 클래스들을 말한다.
참조타입의 객체는 실제 데이터는 힙영역에 참조타입 변수는 스택영역에서 실제 객체의 주소를 저장합니다.
해서 객체 사용시마다 스택 영역에서 객체의 주소를 불러와 사용하게 됩니다.
이후 Garbage Collector가 돌면서 메모리를 해제한다.
자바는 엄밀히 말해 ‘Call by Value’만을 사용한다.
Call by Value는 함수가 호출될 때 값을 전달해주고, Call by Reference는 함수가 호출될 때 주소를 전달해준다.
그래서 Call by Value는 함수를 호출할 때 전달된 값의 복사본을 생성하기 때문에 값을 어떻게 지지고 볶든 local value의 성격을 지니기 때문에 함수가 종료한 후에 값이 변화되지 않지만, Call by Reference는 주소를 바로 참조하기 때문에 호출한 함수에서의 변경이 함수가 종료된 이후에도 남는다.
그런데 JAVA는 int, float, double등 primitive type에 대해서는 Call by Value이고, array나 class instance등은 Call by Reference로 작동한다라고들 한다. 이게 무슨 이야기인지, 아래 예제를 보도록 하자.
public class CallByValueTest {
public static void main(String args[]) {
Obj o1 = new Obj(1);
Obj o2 = new Obj(2);
System.out.println("Before ==> " + o1.value + "," + o2.value); // 1, 2
changeValue(o1, o2);
System.out.println("After ==> " + o1.value + "," + o2.value); // 3, 2
}
public static void changeValue(Obj param1, Obj param2) {
// param1, param2 는 각각 o1, o2가 들고 있던 "참조값"의 복사본
param1.value = 3; // 복사된 참조를 통해 실제 Obj(힙)에 가서 value 변경
param2 = param1; // 복사된 참조들끼리의 재할당이므로, 메서드 밖의 o2엔 영향 없음
}
static class Obj {
int value;
Obj(int i) { this.value = i; }
}
}

param2 = param1;로 인해 메서드 내부에서는 param2가 param1과 같은 객체를 가리키게 되었지만,param1/param2는 사라진다. o2는 여전히 new Obj(2)를 가리키고 있으므로 o2.value는 2가 남는다.이처럼 참조 타입도 ‘참조(주소)의 값’을 복사해 전달하므로, 메서드 안에서 재할당해도 원래의 참조 변수에는 영향이 없다.
기본 타입(int, float, double 등)을 메서드에 넘길 경우:
참조 타입(배열, 클래스 인스턴스 등)을 메서드에 넘길 경우:
이 점 때문에,
참고1 : 자바의 Call by Value와 Call by Reference 이해하기
참고2 : JAVA는 Call by Value일까 Call By Reference일까?
상수(Constant)
1. final 키워드를 붙인 변수로, 한 번 설정한 후에는 값을 변경할 수 없다.
2. 선언과 동시에 초기화해야 하며, 주로 대문자로 이름을 짓는다.
3. 의미 있는 이름을 붙여 코드 가독성과 유지보수성을 높인다.
4. 예시:
final int MAX_SPEED = 100;
// MAX_SPEED = 200; // 컴파일 에러 (상수 값은 변경 불가)
리터럴(Literal)
변하지 않는 데이터 자체를 의미, 예: 10, 3.14, "JAVA".
예시:
int number = 10; // 정수 리터럴
long bigNumber = 10L; // long 리터럴
float pi = 3.14f; // float 리터럴
String text = "JAVA"; // 문자열 리터럴
char ch = 'A'; // 문자 리터럴
참고 : 상수와 리터럴 ( constant 와 literal ) 이란?
(1) public
(2) static
클래스 로딩 시점에 초기화
static 영역은 프로그램 시작 시 클래스가 로딩될 때 이미 메모리에 적재된다. static 멤버(필드·메서드)는 객체 없이도 바로 사용 가능하다.객체 생성 없이 호출 가능
static 메서드는 클래스명만으로 호출할 수 있다. MyClass.myStaticMethod(); main 메서드도 이 원리로, 자바 애플리케이션 시작 시점에 객체 없이 바로 호출된다.인스턴스 멤버와 구분
static 메서드 내부에서는 인스턴스 멤버(필드나 메서드)에 직접 접근할 수 없다. (3) void
(4) main(String[] args)
String[] args는 문자열 배열을 의미한다. args는 관례적 표현이며, 다른 이름으로 변경해도 무방하다.이로써 public static void main(String[] args)는 자바 프로그램의 진입점이자 시작점이며, 어디서든 접근 가능하고, 객체 없이 호출되며, 값을 반환하지 않는다는 뜻을 담고 있다.
참조 : 메인메소드 public static void main(String[] args) 를 이해하자
Java에서 직렬화(Serialization)란 자바의 객체를 바이트의 배열로 변환하여 파일, 메모리, 데이타베이스 등을 통해서 스트림(송수신)이 가능하도록 하는 것을 의미한다.
즉, Java 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 Java 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 데이터 변환하는 것을 뜻한다.

물론, Java 시스템간의 데이터 교환을 위해서 XML, JSON 과 같은 직렬화를 사용해도 되지만
Java Serializable을 사용하는 가장 큰 이유는 개발자의 편의를 위해서이다.
복잡한 데이터 구조를 가진 클래스의 객체라도 직렬화 기본 조건만 지키면 큰 작업 없이 바로 직렬화/역직렬화를 가능할 뿐만 아니라 Data Type이 자동으로 맞춰지기 때문에 관련 부분에 대해 큰 신경을 쓰지 않아도 되기 때문이다.
Java에선 직렬화를 위해 Serializable이라는 Marker interface를 지원하고 있다.
package java.io;
public interface Serializable {
}