자바와 C++의 비교
1. 개요
자바와 C++는 서로 다른 설계 목표를 가진 프로그래밍 언어이다. C++는 시스템 및 애플리케이션 프로그래밍을 위해 설계되었으며, C 언어를 확장하여 객체 지향 프로그래밍, 예외 처리, 제네릭 프로그래밍 등을 지원한다. 반면, 자바는 구현 종속성을 최소화하고 이식성을 높이기 위해 설계된 범용 객체 지향 언어이며, 자바 가상 머신(JVM)을 통해 실행된다. C++는 C와의 호환성을 유지하며, 성능을 중시하는 반면, 자바는 플랫폼 독립성, 보안, 단순성을 강조한다. 두 언어는 문법, 의미론, 리소스 관리, 라이브러리, 런타임, 템플릿/제네릭스, 성능 등 다양한 측면에서 차이를 보인다. C++는 강력하지만 복잡하며, 자바는 배우기 쉽지만, 플랫폼의 모든 기능을 활용하기는 어렵다.
-
프로그래밍 언어의 비교 -
예외 처리 문법
예외 처리 문법은 프로그래밍 언어에서 프로그램 실행 중 발생할 수 있는 예외 상황을 처리하기 위해 try, catch, finally, throw 등의 키워드를 사용하여 예외 처리 방식에 차이를 보이며, 프로그램의 흐름을 제어하고 자원 관리를 수행하는 데 사용된다. -
프로그래밍 언어의 비교 -
비주얼 베이직 닷넷과 C 샤프의 비교
C#과 VB.NET은 마이크로소프트의 .NET 프레임워크를 위한 주요 프로그래밍 언어이며, C#은 C 언어 기반, VB.NET은 BASIC 언어에서 파생되었고, 구문과 개발 환경에서 차이를 보이지만 동일한 .NET 런타임을 공유하며, 다중 언어 지원을 위해 공통 중간 언어로 컴파일된다. -
C++ -
헤더 파일
헤더 파일은 프로그래밍 언어에서 코드 재사용성, 모듈화, 컴파일 시간 단축에 기여하며 함수 프로토타입, 변수 선언 등을 포함하고 `#include` 지시어로 소스 코드에 포함되어 사용되는 파일이다. -
C++ -
소멸자 (컴퓨터 프로그래밍)
소멸자는 객체가 메모리에서 제거되기 직전에 호출되는 멤버 함수로, 객체 자원 해제 및 정리 작업을 수행하며, C++ 등 여러 언어에서 구현되고 메모리 누수 방지에 기여한다. -
자바 (프로그래밍 언어) -
자바 애플릿
-
자바 (프로그래밍 언어) -
자바FX
JavaFX는 자바 기반의 UI 구축 플랫폼으로, 다양한 플랫폼을 지원하며 풍부한 UI 기능들을 제공하고, Java 8부터 JDK에 포함되었다가 JDK 11부터 분리되어 관리된다.
2. 설계 목표
C++과 자바 언어의 차이는 각 언어가 탄생한 역사적 배경과 초기 설계 목표에서부터 찾을 수 있다. C++는 C 언어를 기반으로 효율적인 실행과 시스템 제어 능력에 중점을 두고 확장된 반면, 자바는 플랫폼 독립적인 네트워크 컴퓨팅 환경을 목표로 개발되어 이식성과 안전성을 강조했다.
이처럼 서로 다른 개발 목적은 두 언어의 기본적인 설계 원칙, 개발 방향, 그리고 기능과 성능 사이의 트레이드오프 결정에 근본적인 차이를 가져왔다. 이러한 설계 목표의 차이는 이후 각 언어의 구체적인 특징과 발전 방향에 지속적으로 영향을 미치게 된다.
2.1. C++의 설계 목표
C++는 이름에서 나타나듯이 C 언어를 확장하여 개발된 언어이다. C++의 핵심 설계 목표 중 하나는 C 언어와의 호환성을 유지하는 것이었다. 이를 통해 C 언어의 장점(기계어나 어셈블러에 준하는 고속성 및 하드웨어 조작성 등)을 전혀 손상시키지 않도록 설계되었으며, 기존의 방대한 C 언어 코드 자산을 활용하고 C 라이브러리와의 상호 운용성을 확보하는 데 중점을 두었다.
또 다른 중요한 목표는 효율적인 실행이다. C++는 네이티브 코드로 컴파일되어 실행되므로, 하드웨어에 가까운 저수준 제어를 통해 고성능을 달성할 수 있도록 설계되었다. 이는 시스템 프로그래밍이나 성능이 중요한 응용 프로그램 개발에 C++가 적합한 이유 중 하나이다.
C++는 단일 패러다임에 얽매이지 않고 다양한 프로그래밍 방식을 지원하는 것을 목표로 한다. C 언어로부터 계승한 절차적 프로그래밍뿐만 아니라, 객체 지향 프로그래밍, 제네릭 프로그래밍 등을 포괄적으로 지원한다. 또한 예외 처리, RAII(Resource Acquisition Is Initialization) 같은 기능을 도입하여 프로그래밍의 유연성과 표현력을 높였다. 범용 컨테이너와 알고리즘을 포함하는 C++ 표준 라이브러리(STL 포함)도 추가되어 개발 생산성을 향상시켰다.
이러한 설계 목표에 따라 C++는 운영 체제, 임베디드 시스템, 고성능 컴퓨팅, 게임 개발 등 시스템 프로그래밍 및 응용 프로그램 개발 분야에서 널리 사용된다. C++는 프로그래머에게 높은 수준의 제어 권한과 유연성을 제공하지만, 이는 상대적으로 복잡성을 높이고 타입 안전성이나 메모리 관리 측면에서 프로그래머의 세심한 주의를 요구하는 트레이드오프를 가진다. 즉, C++는 프로그래머를 신뢰하며 최대한의 성능과 유연성을 발휘할 수 있도록 하는 데 초점을 맞춘 언어라고 할 수 있다.
2.2. Java의 설계 목표
자바는 처음 가전제품에 탑재되어 네트워크 컴퓨팅을 지원하기 위해 개발되었다. 개발 초기부터 C++와는 다른 목표를 가지고 설계되었으며, 주요 설계 목표는 다음과 같다.
* 플랫폼 독립성: 자바의 가장 중요한 목표 중 하나는 "한 번 작성하면 어디서든 실행된다"(Write Once, Run Anywhere영어)는 원칙을 구현하는 것이다. 이는 JVM(Java Virtual Machine) 위에서 코드가 실행되기 때문에 가능하다. JVM은 운영 체제나 하드웨어에 상관없이 동일한 실행 환경을 제공하여 높은 이식성을 보장한다.
* 보안: 자바는 네트워크 환경에서의 사용을 염두에 두고 설계되었기 때문에 컴퓨터 보안을 중요하게 고려했다. 코드가 JVM이라는 보호된 환경 내에서 실행되므로, 시스템에 직접 접근하여 발생할 수 있는 위험을 줄였다.
* 단순성: C++에 비해 배우고 사용하기 쉬운 언어를 목표로 했다. C나 C++와 유사한 문법을 채택하여 기존 개발자들이 쉽게 접근할 수 있도록 했지만, 포인터와 같은 복잡하고 오류 발생 가능성이 높은 기능은 제거했다. 또한, 자동 메모리 관리(가비지 컬렉션) 기능을 도입하여 개발자가 메모리 누수 문제에 신경 쓰지 않고 프로그래밍에 집중할 수 있도록 했다.
* 객체 지향: 자바는 객체 지향 프로그래밍 언어로 설계되었다. 모든 코드는 클래스 내부에 작성되어야 하며, 상속, 다형성, 캡슐화 등 객체 지향의 핵심 개념을 적극적으로 지원한다.
* 풍부한 표준 라이브러리: 자바는 하위 플랫폼의 차이를 추상화하는 방대하고 강력한 표준 라이브러리를 제공한다. 이를 통해 개발자는 운영 체제나 하드웨어에 대한 깊은 이해 없이도 네트워크 프로그래밍, 데이터베이스 연동, GUI 개발, 멀티스레딩, 분산 컴퓨팅 등 다양한 기능을 쉽게 구현할 수 있다. 이는 특히 웹 애플리케이션 및 엔터프라이즈 시스템 개발에 강점을 보인다.
2.3. 설계 목표 비교
C++과 자바 언어의 차이는 각 언어의 탄생 역사와 설계 목표에서 비롯된다.
* C++는 C 언어를 확장하여 만들어졌으며, 이름에서도 그 유래를 알 수 있다. C++는 본래 절차적 프로그래밍 언어였던 C에 객체 지향 프로그래밍 개념을 도입하고, 효율적인 실행 성능을 목표로 설계되었다. 주요 특징으로는 정적 타입 검사, 예외 처리, RAII(Resource Acquisition Is Initialization, 자원 획득은 초기화이다), 제네릭 프로그래밍 지원 등이 있다. 또한, 범용 컨테이너와 알고리즘을 포함하는 C++ 표준 라이브러리가 추가되어 개발 생산성을 높였다. C++는 시스템 프로그래밍이나 고성능 애플리케이션 개발에 주로 사용된다.
* 자바는 처음에는 가전제품과 같은 임베디드 시스템에 탑재되어 네트워크 컴퓨팅을 지원하기 위한 목적으로 개발되었다. 자바 가상 머신(JVM) 위에서 실행되기 때문에 높은 이식성과 안전성을 가지는 것이 특징이다. 또한, 운영체제나 하드웨어 같은 하위 플랫폼의 차이를 거의 완벽하게 추상화해주는 방대한 표준 라이브러리를 제공한다. 자바는 C++와 유사한 문법을 채택했지만, 직접적인 코드 호환성은 없다. 설계 당시부터 개발자들이 사용하기 쉽고 이해하기 쉬운 언어를 목표로 했다.
두 언어는 개발 목적이 달랐기 때문에 설계 원리, 개발 방침, 그리고 성능과 편의성 사이의 트레이드오프에서 차이가 나타난다. 주요 차이점은 다음과 같다.
| 특징 | C++ | 자바 |
|---|
| 기원/호환성>C를 확장. C 소스 코드와 하위 호환성 (일부 예외 제외). | C++와 유사한 문법 사용, 직접적인 C/C++ 호환성 없음. |
| 실행 환경>네이티브 기계 코드로 컴파일되어 실행. | 자바 가상 머신 (JVM) 위에서 바이트코드로 실행. |
| 개발 패러다임>절차적 프로그래밍, 객체 지향 프로그래밍, 제네릭 프로그래밍, 함수형 프로그래밍, 템플릿 메타프로그래밍. 멀티 패러다임 선호. | 객체 지향 프로그래밍 강력 권장. 절차적 프로그래밍, 함수형 프로그래밍 (Java 8+), 제네릭 프로그래밍 (Java 5+) 지원. |
| 메모리 관리>명시적 관리 (`new`/`delete`), RAII, 스마트 포인터. 선택적 가비지 컬렉션 라이브러리 사용 가능. | 자동 가비지 컬렉션. |
| 자원 관리>수동 또는 RAII (자동 수명 기반 관리). | 수동 또는 `try-with-resources` (Java 7+). 파이널라이저 사용 비권장. |
| 시스템 접근>저수준 시스템 접근 및 직접적인 시스템 라이브러리 호출 가능. | 자바 네이티브 인터페이스 (JNI) 또는 Java 네이티브 액세스 (JNA) 통해 네이티브 코드 호출 가능 (안전성 및 성능 저하 가능성 있음). JVM 위에서 실행되어 직접 접근 제한. |
| 경계 검사>선택적 자동 경계 검사 (예: `std::vector::at()`). | 항상 자동 경계 검사 수행 (최적화로 제거될 수 있음). |
| 부호 없는 연산>네이티브 부호 없는 산술 지원. | 기본적으로 지원 안 함 (Java 8에서 일부 변경). |
| 매개변수 전달>값 또는 참조에 의한 전달. | 항상 값에 의한 전달 (객체 참조값 전달 가능). |
| 타입 안전성>명시적 타입 변환 허용, 일부 암시적 변환 허용 (C 호환성). | 엄격한 타입 안전성 (확장 변환 제외). |
| 표준 라이브러리>C++ 표준 라이브러리: 상대적으로 제한적 범위. Boost 등 외부 라이브러리 활용. | 방대하고 기능 풍부한 표준 라이브러리 (GUI, 네트워크, 스레딩 등 포함). |
| 연산자 오버로딩>대부분 연산자 오버로딩 가능. | 불가능 (`String`의 `+`, `+=` 제외). |
| 상속>단일 및 다중 상속, 가상 상속 지원. | 클래스의 단일 상속만 지원. 인터페이스를 통한 다중 상속 구현. |
| 제네릭/템플릿>컴파일 타임 템플릿. 튜링 완전 메타프로그래밍 가능. | 제네릭 (컴파일 시 타입 소거). |
| 이식성>한 번 작성하면 어디서나 컴파일 (WOCA). 플랫폼별 재컴파일 필요. | 한 번 작성하면 어디서나 실행 (WORA/WORE). JVM이 설치된 곳이면 실행 가능. |
이처럼 C++와 자바는 각기 다른 설계 목표를 가지고 개발되었으며, 이러한 목표의 차이가 언어의 구조, 기능, 장단점 등 다양한 측면에서 뚜렷한 차이를 만들어냈다.
3. 언어의 특징
C++는 플랫폼 특정 라이브러리에서 일반적으로 사용할 수 있는 많은 기능에 대해 교차 플랫폼 접근을 제공하는 반면, 자바에서 기본 운영 체제 및 하드웨어 기능에 직접 접근하려면 Java 네이티브 인터페이스를 사용해야 한다.
3.1. 문법
자바의 문법은 간단한 LALR 파서로 해석할 수 있는 문맥 자유 문법인 반면, C++의 문법 해석은 더 복잡하다. 예를 들어, C++에서 `Foo<1>(3);`와 같은 코드는 `Foo`가 변수인지 클래스 템플릿인지에 따라 해석이 달라진다. 변수라면 연속된 비교 연산으로, 클래스 템플릿이라면 객체 생성으로 해석된다.
C++는 네임스페이스 수준에서 상수, 변수, 함수를 선언할 수 있지만, 자바에서는 모든 선언이 반드시 클래스나 인터페이스 내부에 속해야 한다.
C++의 `const` 키워드는 데이터가 '읽기 전용'임을 명시하며 자료형에 적용된다. 반면 자바의 `final` 키워드는 변수가 다시 할당될 수 없음을 나타낸다. 기본 자료형에서는 `const int`와 `final int`처럼 비슷하게 동작하지만, 복합 객체에서는 차이가 있다. C++11부터는 컴파일 시 결정되는 상수 표현식을 위한 `constexpr` 지정자도 지원한다. 자바에서는 보통 `static final` 필드로 상수를 정의한다.
| C++ | Java |
|---|---|
또한 C++에서는 `const`로 수식된 멤버 함수 내에서는 멤버 변수를 변경할 수 없도록 강제하여 const-정확성을 지원한다 (
mutable 키워드 제외). 자바에는 이에 직접 대응하는 기능이 없다. 자바의 `final`로 수식된 메서드는 하위 클래스에서 오버라이드할 수 없으며, `final` 클래스는 상속될 수 없다. C++11에서도 `final` 지정자가 추가되어 메서드 오버라이드 및 클래스 상속을 금지할 수 있다.C++는
goto 문을 지원하지만, 이는 스파게티 코드를 유발할 수 있어 사용이 권장되지 않는다. 자바는 goto 문을 지원하지 않고, 대신 레이블(labeled) break 문과 continue 문을 제공하여 구조적 프로그래밍을 강제한다.C++는 자바에는 없는 저수준 기능을 제공한다. C++의 포인터를 이용하면 특정 메모리 주소를 직접 조작할 수 있으며, 많은 C++ 컴파일러는 인라인 어셈블러를 지원한다. 자바에서 이러한 저수준 작업이 필요할 경우, 외부 라이브러리를 작성하고 자바 네이티브 인터페이스(JNI)를 통해 접근해야 하며, 이는 호출 시 상당한 오버헤드를 발생시킨다.
C++는 기본 자료형 간의 암시적 형 변환을 폭넓게 허용하며, 사용자 정의 자료형에 대한 암시적 변환 규칙도 정의할 수 있다. 자바는 기본 자료형 간에는 크기가 커지는 방향(widening conversion)의 암시적 변환만 허용하고, 그 외에는 명시적인 형 변환(cast)이 필요하다. 이 차이는 조건문에서도 나타나는데, 예를 들어
if (a = 5) 같은 코드는 C++에서는 컴파일될 수 있지만 (대부분 컴파일러 경고 발생), 정수형을 불리언으로 암시적 변환할 수 없는 자바에서는 컴파일 오류가 발생한다.함수나 메서드에 인자를 전달할 때, C++는 값 호출(pass-by-value)과 참조 호출(pass-by-reference)을 모두 지원한다. 자바는 항상 값 호출 방식을 사용한다. 기본형(primitive type)은 값이 그대로 복사되어 전달되고, 객체(참조형, reference type)는 객체를 가리키는 참조 값이 복사되어 전달된다.
자바의 내장 자료형(primitive type)은 자바 가상 머신(JVM) 명세에 의해 크기와 범위가 고정되어 있어 플랫폼 독립성을 보장한다. 예를 들어 자바의
char는 항상 16비트 유니코드 문자이다. 반면 C++의 내장 자료형은 최소 범위만 표준으로 정해져 있고, 실제 크기와 표현 방식은 컴파일러와 플랫폼에 따라 달라질 수 있다. 이는 특정 플랫폼에서 C++가 더 효율적인 코드를 생성할 수 있게 하지만, 플랫폼 간 이식성에서는 자바가 유리하다.부동소수점 연산에 있어서도 C++는 플랫폼 종속적인 동작을 보일 수 있는 반면, 자바는
strictfp 키워드를 사용하여 플랫폼 간 동일한 결과를 보장하는 엄격한 부동소수점 모델을 선택할 수 있다 (약간의 성능 저하 가능성 있음).C++의 포인터는 메모리 주소 자체를 값으로 다루며 직접적인 주소 연산이 가능하다. 자바에는 포인터가 없고 대신 객체에 대한 참조(reference)만 존재하며, 메모리 주소에 직접 접근하거나 조작하는 것은 허용되지 않는다. C++에서는 함수나 메서드를 가리키는 함수 포인터나 펑터를 사용할 수 있지만, 자바에서는 객체 참조나 인터페이스 참조를 통해 유사한 기능을 구현한다.
자원 관리에 있어서 C++는 주로 RAII(Resource Acquisition Is Initialization) 기법을 사용한다. 객체가 생성될 때 자원을 획득하고, 객체가 스코프를 벗어나 소멸자가 호출될 때 자원을 자동으로 해제하는 방식이다. 자바는 가비지 콜렉션(GC)을 통해 더 이상 사용되지 않는 객체의 메모리를 자동으로 회수한다. 하지만 메모리 외의 다른 시스템 자원(파일 핸들, 네트워크 소켓 등)은 GC가 언제 동작할지 보장되지 않으므로,
try-finally 또는 `try-with-resources` 구문을 사용하여 명시적으로 해제해 주어야 한다. 이 차이로 인해 C++에서는 허상 포인터(dangling pointer) 문제가 발생할 수 있지만, 자바에서는 GC가 참조 중인 객체를 해제하지 않으므로 이 문제가 없다. 반면, 자바는 메모리가 아닌 다른 자원의 누수 가능성에 상대적으로 취약할 수 있다. C++에서는 초기화되지 않은 객체가 생성될 수 있지만(쓰레기 값 보유), 자바는 기본값(0, null 등)으로 강제 초기화된다.C++는 프로그래머가 연산자 오버로딩을 통해 사용자 정의 타입에 대해 연산자의 동작을 재정의할 수 있다. 자바는 연산자 오버로딩을 지원하지 않으며, 문자열 연결을 위한
+ 연산자만 예외적으로 오버로딩되어 있다.자바는 리플렉션과 동적 로딩을 위한 표준 API를 제공하여 런타임에 클래스 정보를 얻거나 코드를 로드하는 것이 용이하다.
제네릭 프로그래밍을 위해 자바는 제네릭(Generics)을, C++는 템플릿(Templates)을 사용한다. C++ 템플릿은 타입뿐만 아니라 값이나 다른 템플릿도 인자로 받을 수 있어 더 강력한 메타 프로그래밍 기능을 제공한다.
자바에서는 기본 자료형은 항상 값 의미론으로 동작하고, 객체(사용자 정의 자료형)는 항상 참조 의미론으로 동작한다. C++에서는 모든 자료형이 기본적으로 값 의미론을 가지지만, 포인터나 참조를 사용하여 참조 의미론으로 객체를 다룰 수 있다. 아래 표는 이 차이를 보여준다.
| C++ | 자바 |
|---|---|
C++는 클래스의 다중 상속을 지원하지만, 죽음의 다이아몬드와 같은 복잡성 문제가 발생할 수 있다. 자바는 클래스의 단일 상속만 허용하는 대신, 여러 개의 인터페이스를 구현(implement)할 수 있도록 하여 타입의 다형성은 지원하면서 구현 상속의 복잡성을 피한다. 자바의 인터페이스는 C++의 순수 가상 함수(pure virtual function)를 포함하는 추상 클래스와 유사한 역할을 한다.
자바는 언어와 표준 라이브러리 차원에서 멀티스레드 프로그래밍을 지원하며,
synchronized 키워드를 통해 비교적 간단하게 뮤텍스 잠금을 구현할 수 있다. C++는 C++11 표준부터 공식적인 메모리 모델과 멀티스레딩 지원 라이브러리를 도입했지만, 이전에는 플랫폼별 라이브러리에 의존해야 했다.C++의 멤버 함수는 기본적으로 비가상(non-virtual) 함수이며,
virtual 키워드를 사용해야 동적 디스패치(dynamic dispatch)가 가능하다 (옵트인 방식). 자바의 메서드는 기본적으로 가상(virtual) 함수이며, final 키워드를 사용하여 비가상 함수로 만들 수 있다 (옵트아웃 방식).C++의 열거형(enum)은 정수형으로 암시적 변환이 가능하지만, C++11에서는 타입 안전성을 강화한 강력한 타입의 열거형(strongly-typed enum)이 도입되었다. 자바의 열거형은 클래스처럼 취급되며, 필드와 메서드를 가질 수 있다.
기타 문법 및 기능 차이:
* 기본 인자: C++는 함수/메서드의 인자에 기본값을 허용하지만, 자바는 허용하지 않는다. 자바에서는 메서드 오버로딩으로 유사하게 구현할 수 있다.
* 최소 컴파일 단위: C++는 함수가 최소 단위이지만, 자바는 클래스가 최소 단위이다.
* 전처리기: C++는 전처리기를 지원하여 매크로 정의, 조건부 컴파일 등이 가능하지만, 자바는 지원하지 않는다.
* 파일 분할: 자바는 패키지 시스템과 클래스 파일 임포트를 사용하고, C++는 헤더 파일 인클루드를 사용한다.
* 컴파일된 코드 크기: 일반적으로 자바 바이트코드가 C++ 기계어보다 간결하여 파일 크기가 작다.
* 컴파일 및 실행: C++는 보통 기계어로 직접 컴파일되어 실행되지만, 자바는 바이트코드로 컴파일된 후 자바 가상 머신(JVM) 위에서 JIT 컴파일 등을 통해 실행된다.
* 배열 경계 검사: 자바는 배열 범위 검사를 강제하여 안정성을 높이지만 약간의 성능 저하가 있을 수 있다. C++ 네이티브 배열은 경계 검사를 하지 않아 빠르지만 잠재적으로 위험하며, STL 컨테이너는 선택적 검사를 제공한다.
* 나눗셈 연산: 자바와 C++11 이후의 C++는 정수 나눗셈 시 0을 향해 버림(truncate toward zero)하도록 정의되어 있지만, 이전 C++ 표준에서는 구현에 따라 달랐다.
* 증감 연산자 (`++`, `--`): C++에서는 lvalue를 직접 수정하지만, 자바의 경우 래퍼 클래스(Wrapper class) 객체(예: `Integer`)에 사용하면 새로운 객체가 할당되어 참조가 변경될 수 있다.
3.2. 의미론 (Semantics)
C++는 기본 자료형 사이에 암시적 형 변환을 허용하며, 사용자 정의 자료형에 대한 암시적 형 변환도 정의할 수 있다. 이는 C와의 호환성을 위한 것이지만, 일부 축소 변환도 포함될 수 있어 주의가 필요하며 대부분의 컴파일러는 경고를 발생시킨다. 반면, 자바에서는 기본 자료형 사이에 정밀도가 커지는 확대 변환만 암시적으로 허용하며, 정밀도가 감소하는 축소 변환은 명시적인 캐스트 구문이 필요하다.
이러한 차이는 조건문(`if`, `while`, `for`의 탈출 조건)에서도 나타난다. C++에서는 조건식의 결과가 반드시 `bool` 타입일 필요는 없으며, 문맥적으로 `bool`로 변환될 수 있는 값이면 된다. 예를 들어 정수형, 부동소수점형, 포인터 등은 0(또는 null)이면 `false`, 그 외에는 `true`로 암시적 변환이 가능하다. 이 때문에 `if (a = 5)`와 같은 대입문이 포함된 조건식도 문법적으로 허용되지만 (대부분 컴파일러 경고 발생), 이는 `if (a == 5)`의 오타일 가능성이 있다. 자바에서는 조건식의 결과가 반드시 `boolean` 타입이어야 하며, 다른 타입에서 `boolean`으로의 암시적 변환이나 단순 캐스트는 허용되지 않는다. 따라서 `if (a = 5)`와 같은 코드는 자바에서 컴파일 오류를 발생시켜 의도치 않은 실수를 방지하는 데 도움이 된다. C/C++에서 변수 `x`가 0 또는 `null`이 아님을 확인하는 `if (x)`와 같은 코드를 자바로 옮길 때는 `if (x != 0)` 또는 `if (x != null)`과 같이 명시적인 비교가 필요하다.
함수에 인자를 전달하는 방식에서도 차이가 있다. C++는 값 호출(call-by-value)과 참조 호출(call-by-reference) 방식을 모두 지원한다. 반면 자바에서는 모든 인자는 항상 값 호출 방식으로 전달된다. 객체 타입(참조 타입)의 경우, 객체 자체가 복사되는 것이 아니라 객체를 가리키는 참조 값이 복사되어 전달된다.
자바의 내장 기본 자료형(primitive types)은 JVM 명세에 의해 크기와 값의 범위가 명확하게 정의되어 있어 플랫폼 간 일관성을 보장한다. 예를 들어, 자바의 `char` 타입은 항상 16비트 유니코드(UTF-16) 문자를 나타낸다. 반면 C++의 내장 자료형은 표준에서 최소한의 범위만 규정하고 있으며, 실제 크기와 표현 방식(예: 비트 수, 부호 표현 방식)은 컴파일러나 실행되는 플랫폼에 따라 달라질 수 있다. 예를 들어, C++의 `char`는 1바이트(최소 8비트) 크기만 보장되며, `wchar_t`의 크기나 인코딩 방식도 플랫폼마다 다를 수 있다. 다만, C++11부터는 크기와 내부 표현이 고정된 정수형(예: `std::int32_t`)이 표준 라이브러리에 추가되었고, C++20에서는 부호 있는 정수의 내부 표현이 2의 보수임이 규정되었다. C++11에서는 UTF-16/UTF-32를 지원하는 `char16_t`/`char32_t` 타입과 관련 문자열 클래스도 추가되었다.
부동소수점 연산에 있어서도 C++는 플랫폼의 구현에 따라 결과가 달라질 수 있지만, 대부분의 현대 컴파일러는 IEEE 754 표준을 준수한다. 자바는 `strictfp` 키워드를 사용하여 플랫폼 간에 동일한 부동소수점 연산 결과를 보장하는 모드를 선택할 수 있으나, 이는 성능 저하를 유발할 수 있다.
C++는 포인터를 통해 메모리 주소를 직접 접근하고 조작할 수 있는 저수준 기능을 제공한다. 포인터의 포인터를 만들거나, 함수를 가리키는 함수 포인터 또는 펑터를 사용하는 것도 가능하다. 자바에는 포인터 개념이 없으며, 객체나 배열에 대한 참조(reference)만 존재한다. 이 참조는 메모리 주소에 직접 접근하거나 포인터 연산을 수행하는 것을 허용하지 않으며, 오직 객체나 배열에 접근하는 용도로만 사용된다. 자바에서는 메서드 참조(Java 8 이상)나 인터페이스 참조를 통해 함수 포인터와 유사한 기능을 구현한다.
C++는 프로그래머가 직접 연산자 오버로딩을 정의하여 사용자 정의 타입에 대해 내장 연산자(+, -, *, / 등)의 동작을 지정할 수 있다. 자바는 연산자 오버로딩을 지원하지 않으며, 예외적으로 문자열 연결을 위한 `+` 및 `+=` 연산자만 오버로딩되어 있다.
자바는 리플렉션과 동적 로딩을 지원하는 표준 API를 제공하여, 실행 시간에 클래스 정보를 얻거나 코드를 동적으로 로드하고 실행하는 것이 가능하다. C++는 언어 표준 차원에서 이러한 기능을 직접 제공하지는 않지만, 운영체제의 동적 라이브러리 로딩 기능 등을 통해 유사한 작업을 수행할 수 있다.
제네릭 프로그래밍을 위해 자바는 제네릭(Generics)을, C++는 템플릿(Templates)을 지원한다. 두 기능은 비슷한 목적(코드 재사용성 및 타입 안전성 향상)을 가지며 유사한 문법을 사용하기도 하지만, 구현 방식과 기능 범위에서 상당한 차이가 있다. 자바 제네릭은 주로 타입 안전한 컬렉션(컨테이너)을 만드는 데 사용되며, 컴파일 시 타입 정보가 소거되는(type erasure) 방식으로 동작한다. 반면 C++ 템플릿은 컴파일 시점에 템플릿 인자에 따라 실제 코드를 생성하는 방식으로 동작하며, 제네릭 프로그래밍뿐만 아니라 템플릿 메타프로그래밍과 같은 더 광범위한 용도로 활용될 수 있다. 두 기능의 주요 차이점은 다음과 같다.
:
| C++ 템플릿 | 자바 제네릭 |
|---|---|
| 클래스, 함수, 별칭, 변수를 템플릿화할 수 있다. | 클래스와 메서드를 제네릭화할 수 있다. |
| 매개변수는 타입, 정수 값, 문자 리터럴, 다른 템플릿 등 다양하게 사용될 수 있다. | 매개변수는 참조 타입(기본형의 경우 박싱된 타입, 예: `Integer`)만 가능하다. |
| 컴파일 시 각 템플릿 인자 조합에 대해 별도의 코드가 생성된다(인스턴스화). | 컴파일 시 타입 정보가 제거되고(타입 소거), 모든 타입 인자에 대해 단일 코드가 사용된다. |
| 다른 인자로 인스턴스화된 템플릿 객체는 런타임에 서로 다른 타입을 갖는다. | 다른 타입 인자를 사용해도 런타임에는 동일한 타입을 갖는다. 타입 소거로 인해 제네릭 타입 인자만 다른 메서드 오버로딩은 불가능하다. |
| 템플릿 정의는 일반적으로 헤더 파일에 포함되어 컴파일 시점에 접근 가능해야 한다(C++11 `extern template` 예외). | 컴파일된 클래스 파일의 시그니처 정보만으로 사용 가능하다. |
| 특정 타입 인자에 대해 별도의 구현을 제공하는 템플릿 특수화가 가능하다. | 제네릭은 특수화될 수 없다. |
| 템플릿 매개변수에 기본 인수를 지정할 수 있다. | 제네릭 타입 매개변수는 기본 인수를 가질 수 없다. |
| 와일드카드 미지원(C++11 `auto` 키워드로 일부 대체 가능). | 와일드카드(`?`)를 사용하여 타입 매개변수의 범위를 유연하게 지정할 수 있다. |
| 타입 매개변수의 제약 조건은 메타프로그래밍이나 C++20의 개념(Concepts)을 통해 설정 가능하다. | `extends`(상한) 및 `super`(하한) 키워드를 사용하여 타입 매개변수의 경계를 명시적으로 지정할 수 있다. |
| 템플릿 매개변수 타입으로 객체를 직접 인스턴스화할 수 있다. | 타입 매개변수로는 객체를 직접 인스턴스화할 수 없다(리플렉션 제외). |
| 타입 매개변수는 정적 멤버(메서드, 변수)에 사용될 수 있다. | 타입 매개변수는 정적 멤버에 사용될 수 없다. |
| 서로 다른 타입 인자로 인스턴스화된 템플릿 간에는 정적 변수가 공유되지 않는다. | 서로 다른 타입 인자를 가진 제네릭 클래스 인스턴스 간에도 정적 변수는 공유된다. |
| 타입 제약 조건 강제가 약하며, 잘못된 타입 사용 시 템플릿 내부 코드에서 컴파일 오류가 발생할 수 있다(C++20 개념으로 개선). | 타입 제약 조건 강제가 강하며, 잘못된 타입 사용 시 해당 사용 지점에서 컴파일 오류가 발생한다. 타입 안전성은 높지만 유연성은 상대적으로 낮다. |
| 템플릿은 튜링 완전하다(템플릿 메타프로그래밍). | 제네릭도 튜링 완전하다. |
C++는 클래스의 다중 상속을 지원하여 하나의 클래스가 여러 부모 클래스로부터 멤버를 상속받을 수 있다. 다이아몬드 문제와 같은 상태 중복 상속 문제를 해결하기 위해 가상 상속 기능도 제공한다. 자바에서는 클래스는 오직 하나의 클래스만 상속할 수 있는 단일 상속 모델을 따른다. 대신, 여러 개의 인터페이스를 구현(implement)함으로써 타입의 다중 상속과 유사한 효과를 얻을 수 있다. 자바 8부터는 인터페이스에 기본 메서드(default method)를 정의할 수 있게 되어 제한적인 형태의 구현 다중 상속도 가능해졌지만, 상태(멤버 변수)의 다중 상속은 여전히 불가능하다.
자바는 인터페이스와 클래스를 명확하게 구분하는 반면, C++에서는 모든 멤버 함수가 순수 가상 함수(pure virtual function)인 클래스를 정의하고 이를 다중 상속함으로써 인터페이스와 유사한 역할을 수행하게 할 수 있다.
C++의 `const` 키워드와 자바의 `final` 키워드는 유사해 보이지만 의미가 다르다. C++의 `const`는 해당 변수나 객체가 '읽기 전용'임을 나타낸다. `const`로 선언된 객체는 그 멤버 변수의 값을 변경할 수 없다. 자바의 `final`은 변수에 새로운 값이나 참조를 다시 할당할 수 없음을 의미한다. 기본 자료형(primitive type) 변수에 사용될 때는 C++의 `const`와 유사하게 동작하지만, 참조 타입 변수에 사용될 경우, `final` 변수 자체는 다른 객체를 참조하도록 변경할 수 없지만, 그 변수가 참조하고 있는 객체의 내부 상태(멤버 변수 값)는 변경될 수 있다.
| C++ | 자바 |
|---|---|
const Rectangle r; | final Rectangle r = new Rectangle(); |
r = anotherRectangle; // 오류: r은 상수이므로 재할당 불가 | r = anotherRectangle; // 오류: final 변수 r은 재할당 불가 |
r.x = 5; // 오류: r은 상수 Rectangle이므로 멤버 변경 불가 | r.x = 5; // 가능: r이 참조하는 Rectangle 객체의 멤버 x는 변경 가능 |
C++는 `goto` 문을 지원하여 코드의 특정 레이블로 직접 점프할 수 있다. 자바는 코드의 구조적 제어 흐름을 강조하기 위해 `goto` 문을 지원하지 않는다. 대신, 특정 루프나 `switch` 문을 빠져나가거나(`break`) 다음 반복을 시작(`continue`)할 때 레이블을 사용하여 중첩된 구조에서 원하는 지점으로 제어를 이동시키는 레이블 `break`와 레이블 `continue`를 제공한다.
3.3. 리소스 관리
자바는 자동 가비지 컬렉션(GC)을 통해 메모리를 관리한다. 반면 C++은 프로그래머가 직접 메모리를 관리하거나, RAII(Resource Acquisition Is Initialization) 패턴과 스마트 포인터를 활용하여 관리하는 것이 일반적이다. C++ 표준은 가비지 컬렉션을 허용하지만 필수는 아니며, 실제로는 거의 사용되지 않는다. C++의 스마트 포인터 중 공유 포인터는 주로 참조 카운트 방식을 사용하는데, 이 경우 순환 참조로 인한 문제가 발생할 수 있다.
C++은 임의 크기의 메모리 블록을 직접 할당할 수 있지만, 자바는 객체 생성을 통해서만 메모리를 할당한다. 자바에서 임의 크기의 메모리 블록이 필요하다면 바이트 배열 객체를 생성하여 사용하며, 자바의 배열 또한 객체이다.
자원 해제 방식에서도 차이가 있다. C++는 객체가 스코프를 벗어나거나 명시적으로 삭제될 때 소멸자가 동기적으로 호출되어 자원을 해제한다. 이는 RAII 패턴의 핵심으로, 예외 발생 시에도 스택 되감기(stack unwinding)를 통해 소멸자가 호출되어 자원 해제를 보장한다. 자바는 가비지 컬렉터가 더 이상 참조되지 않는 객체를 비동기적으로 회수하며, 객체 소멸 직전에 파이널라이저가 호출될 수 있다. 하지만 종결자는 호출 시점이 불확실하고 예측 불가능하며 성능 문제를 유발할 수 있어 사용이 권장되지 않고, 자바 9부터는 사용 자제가 권고된다. 종결자는 주로 JVM 외부 자원을 정리해야 하는 매우 제한적인 경우에만 필요하다. 자바에서는 명시적인 자원 해제가 필요할 경우 `try-with-resources` 구문 (자바 7 이상)이나 `try-finally` 구문을 사용해야 하며, `try-with-resources`가 더 간결하고 권장되는 방식이다.
C++에서는 이미 해제된 메모리를 가리키는 댕글링 포인터(dangling pointer) 문제가 발생할 수 있으며, 이를 사용하면 프로그램 오류로 이어진다. 자바는 가비지 컬렉터가 참조 중인 객체를 해제하지 않으므로 이러한 문제가 원칙적으로 발생하지 않는다.
객체 초기화 방식도 다르다. C++에서는 객체를 초기화하지 않고 생성할 수 있으며, 이 경우 예측 불가능한 쓰레기 값을 가질 수 있다. 자바는 모든 객체와 멤버 변수에 대해 기본값(예: 숫자형은 0, 객체 참조는 null)으로 강제 초기화를 수행한다.
메모리 누수 문제에서도 차이가 있다. C++에서는 프로그래머의 실수로 할당된 메모리에 접근할 수 없게 되어(도달 불가능한 객체) 메모리 누수가 발생할 수 있다. 자바의 가비지 컬렉션은 대부분의 메모리 누수를 방지하지만, 더 이상 필요 없는 객체 참조를 명시적으로 제거하지 않거나(오래된 참조), 캐시 관리 부실 등으로 인해 여전히 메모리 누수("의도하지 않은 객체 유지")가 발생할 수 있다. 메모리가 아닌 파일 핸들이나 네트워크 소켓 등 다른 시스템 자원의 누수 가능성은 RAII를 활용하는 C++에 비해 자바가 상대적으로 더 주의를 요구한다.
3.4. 라이브러리
자바는 C++에 비해 상당히 거대한 표준 라이브러리를 가지고 있다. 표준 C++ 라이브러리는 문자열, 컨테이너, IO 스트림과 같이 비교적 일반적인 목적의 컴포넌트만 제공한다. 반면, 자바 표준 라이브러리(Java SE)는 네트워킹, 그래픽 사용자 인터페이스, XML 처리, 로깅, 데이터베이스 접근, 암호학 및 기타 다양한 영역의 컴포넌트를 포함한다. 이러한 기능은 C++에서는 서드 파티 라이브러리나 OS 고유의 API를 통해 구현되는 경우가 많지만, 모든 환경에서 제공된다는 보장은 없다.
C++는 C 언어에 대한 부분적인 하위 호환성을 가지므로, 많은 운영 체제의 API와 같은 C 라이브러리를 직접 사용할 수 있다. 자바에서는 환경 고유 라이브러리가 제공하는 기능 상당 부분을 크로스 플랫폼 환경에서 사용할 수 있도록 풍부한 표준 라이브러리를 통해 제공한다. 하지만 자바에서 네이티브 운영 체제나 하드웨어 기능에 직접 접근하려면 자바 네이티브 인터페이스(JNI)를 사용해야 한다.
3.5. 런타임
C++ 코드는 일반적으로 기계어로 직접 컴파일되어 운영 체제에 의해 실행된다. 반면, 자바 코드는 보통 바이트코드로 컴파일된 후, 자바 가상 머신(JVM)이 이를 인터프리터 방식으로 실행하거나 JIT 컴파일러를 이용해 실행 시점에 기계어로 변환하여 실행한다. 이론적으로 동적 재컴파일은 두 언어 모두에 적용될 수 있으며 특히 자바에 유용할 수 있지만, 현재는 널리 사용되지 않는다.
C++는 배열 범위 검사를 수행하지 않고, 사용되지 않는 포인터나 자유로운 자료형 변환 허용 등 상대적으로 자유로운 표현력을 가지는 대신, 컴파일 시점에 모든 오류를 검출하기 어려워 런타임에 오류가 발생할 위험이 있다. 예를 들어 버퍼 오버플로나 세그먼테이션 폴트와 같은 심각한 문제가 발생할 수 있다. 물론, STL이 제공하는 벡터(vector), 리스트(list), 맵(map)과 같은 고수준의 추상화된 자료구조를 사용하면 이러한 오류 발생 가능성을 줄일 수 있다.
반면, 자바에서는 C++에서 발생할 수 있는 유형의 오류들이 애초에 발생하지 않도록 설계되었거나, 발생하더라도 JVM에 의해 감지되어 예외 처리 메커니즘을 통해 응용 프로그램에 보고된다. 이는 프로그램의 안정성을 높이는 데 기여한다.
특히 배열 접근과 관련하여, 자바는 배열의 범위를 벗어난 접근을 감지하기 위한 경계 검사를 명확히 요구하며, 범위를 벗어난 접근 시 예외를 발생시킨다. 이는 프로그램의 안정성을 높이지만, 모든 접근마다 검사를 수행해야 하므로 실행 속도에 부정적인 영향을 줄 수 있다. 다만, 컴파일러 최적화를 통해 불필요한 경계 검사를 제거하여 성능 저하를 완화하기도 한다. C++의 네이티브 배열은 기본적으로 경계 검사를 수행하지 않아 속도는 빠르지만, 범위를 벗어난 메모리에 접근할 잠재적 위험을 안고 있다. C++ 표준 라이브러리의 `std::vector`와 같은 컨테이너는 `at()` 멤버 함수를 통해 선택적으로 경계 검사를 수행하는 기능을 제공한다. 요약하자면, 자바 배열은 "항상 안전하고, 엄격하게 검사하며, 가능하면 빠르게" 동작하는 것을 목표로 하는 반면, C++ 네이티브 배열은 "항상 빠르고, 검사하지 않으며, 잠재적으로 위험"한 특성을 가진다고 볼 수 있다.
3.6. 템플릿 vs 제네릭스
C++와 자바 둘 다 제네릭 프로그래밍을 지원하기 위한 기능을 갖추고 있다. C++에는 템플릿이 있고 자바에는 제네릭스가 있다. 둘 다 비슷한 문제를 해결하기 위해 만들어졌으며 문법도 비슷하지만, 내부적으로는 상당히 다르다.
| C++ 템플릿 | 자바 제네릭스 |
|---|---|
| 클래스, 함수, 별칭(typedef) 및 변수를 템플릿화할 수 있다. | 클래스와 메서드를 제네릭으로 만들 수 있다. |
| 매개변수는 가변적일 수 있으며, 모든 타입, 정수 값, 문자 리터럴 또는 클래스 템플릿이 될 수 있다. | 매개변수는 박싱된 기본형 타입(예: Integer, Boolean 등)을 포함한 모든 참조 타입이 될 수 있다. |
| 컴파일 시 각 매개변수 집합에 대해 클래스 또는 함수의 별도 인스턴스화가 생성된다. 클래스 템플릿의 경우 사용되는 멤버 함수만 인스턴스화된다. | 클래스 또는 함수의 한 버전이 컴파일되어 모든 타입 매개변수에 대해 작동한다(타입 소거를 통해). |
| 다른 매개변수로 인스턴스화된 클래스 템플릿의 객체는 런타임에 다른 타입을 갖는다(즉, 개별 템플릿 인스턴스화는 개별 클래스이다). | 타입 매개변수는 컴파일 시 소거된다. 다른 타입 매개변수를 가진 클래스 객체는 런타임에 동일한 타입이다. 이는 다른 생성자를 발생시킨다. 이러한 타입 소거로 인해 제네릭 클래스의 다른 인스턴스화를 사용하여 메서드를 오버로드하는 것은 불가능하다. |
| 클래스 또는 함수 템플릿의 구현은 이를 사용하기 위해 번역 단위 내에서 보여야 한다. 이는 일반적으로 정의를 헤더 파일에 두거나 헤더 파일에 포함시키는 것을 의미한다. C++11부터 일부 인스턴스화의 컴파일을 분리하기 위해 extern 템플릿을 사용할 수 있다. | 컴파일된 클래스 파일의 클래스 또는 함수의 시그니처는 이를 사용하기에 충분하다. |
| 템플릿은 특수화될 수 있다. 특정 템플릿 매개변수에 대해 별도의 구현을 제공할 수 있다. | 제네릭은 특수화될 수 없다. |
| 템플릿 매개변수는 기본 인수를 가질 수 있다. C++11 이전에는 템플릿 클래스에만 허용되었고 함수에는 허용되지 않았다. | 제네릭 타입 매개변수는 기본 인수를 가질 수 없다. |
와일드카드 미지원. 대신 반환 타입은 종종 중첩된 typedef로 사용할 수 있다. (또한, C++11은 컴파일 시간에 결정할 수 있는 모든 타입에 대한 와일드카드로 작동하는 키워드 auto를 추가했다.) | 와일드카드는 타입 매개변수로 지원된다. |
타입 매개변수의 경계 설정 및 타입 매개변수 간의 관계 강제는 메타프로그래밍을 통해 효과적으로 가능하다. 또는 C++20부터 std::derived_from 및 기타 개념을 통해 직접 가능하다. | 상한 및 하한에 대해 각각 "extends" 및 "super"를 사용하여 타입 매개변수의 경계를 지원하며, 타입 매개변수 간의 관계 강제를 허용한다. |
| 매개변수 타입으로 객체를 인스턴스화할 수 있다. | (리플렉션을 제외하고) 매개변수 타입으로 객체를 인스턴스화하는 것을 배제한다. |
| 클래스 템플릿의 타입 매개변수는 정적 메서드 및 정적 변수에 사용될 수 있다. | 제네릭 클래스의 타입 매개변수는 정적 메서드 및 정적 변수에 사용할 수 없다. |
| 서로 다른 타입 매개변수의 클래스와 함수 간에는 정적 변수가 공유되지 않는다. | 서로 다른 타입 매개변수의 클래스 인스턴스 간에는 정적 변수가 공유된다. |
| 클래스 및 함수 템플릿은 선언에서 타입 매개변수에 대한 타입 관계를 반드시 강제하지는 않는다. 잘못된 타입 매개변수를 사용하면 컴파일 오류가 발생하며, 종종 이를 호출하는 사용자 코드 대신 템플릿 코드 내에서 오류 메시지가 생성된다. 템플릿 클래스 및 함수의 올바른 사용은 적절한 문서화에 달려 있다. 메타프로그래밍은 추가 노력이 필요하지만 이러한 기능을 제공한다. C++20부터 개념을 사용하여 이러한 기능을 제공할 수 있다. | 제네릭 클래스 및 함수는 선언에서 타입 매개변수에 대한 타입 관계를 강제할 수 있다. 잘못된 타입 매개변수를 사용하면 이를 사용하는 코드 내에서 타입 오류가 발생한다. 제네릭 코드에서 매개변수화된 타입에 대한 연산은 선언으로 안전하다고 보장할 수 있는 방식으로만 허용된다. 이는 유연성을 희생하여 더 큰 타입 안전성을 제공한다. |
| 템플릿은 튜링 완전하다( 템플릿 메타프로그래밍 참조). | 제네릭스도 튜링 완전하다. |
4. 성능
컴파일된 자바 프로그램을 실행할 때는 프로그램 자체뿐만 아니라 자바 가상 머신(JVM)도 함께 실행해야 한다. 반면 컴파일된 C++ 프로그램은 외부 프로그램 없이 독립적으로 실행될 수 있다. 초기 버전의 자바는 C++와 같은 정적으로 컴파일되는 언어에 비해 성능이 상당히 낮았다. 이는 비슷한 형태의 구문이라도 C++에서는 적은 수의 기계 명령어로 컴파일되는 반면, 자바에서는 여러 개의 바이트코드로 변환되고, 이 바이트코드들이 JVM에서 다시 여러 기계 명령어로 해석되어 실행되기 때문이다. 예를 들어, 동일한 배열 요소 증가 연산은 다음과 같이 다른 코드를 생성한다.
| 자바/C++ 구문 | C++ 컴파일러가 생성한 x86 기계어 코드 | 자바 컴파일러가 생성한 바이트 코드 |
|---|---|---|
| `vector[i]++;` | mov edx,[ebp+4h] | aload_1 |
그러나 자바는 장기 실행되는 서버 및 데스크톱 환경을 위해 JIT 컴파일(Just-In-Time) 기술을 발전시켜왔고, 이를 통해 C++와의 성능 차이를 크게 줄였다. JIT 컴파일은 프로그램 실행 중에 바이트코드를 해당 CPU에 맞는 네이티브 기계어로 컴파일하여 실행 속도를 높이는 기술이다.
성능 최적화는 매우 복잡한 문제이므로, C++와 자바 간의 성능 차이를 일반화하여 정량적으로 비교하기는 어렵고, 많은 벤치마크 결과는 특정 환경에 편향되어 있거나 신뢰성이 낮을 수 있다. 두 언어의 근본적인 설계 철학이 다르기 때문에 명확한 우열을 가리기는 힘들다.
간단히 말해, 자바는 유연한 고수준 추상화에 크게 의존하기 때문에 성능 최적화에 있어 본질적인 비효율성과 명확한 한계가 있을 수 있다. 하지만 강력한 JIT 컴파일러는 이러한 문제 중 상당 부분을 완화할 수 있다. 만약 자바 코드의 성능이 부족하다고 판단될 경우, 자바 네이티브 인터페이스(JNI)를 통해 C나 C++로 작성된 네이티브 코드를 호출하여 성능을 개선할 수도 있다.
자바 언어 자체의 설계에서 비롯될 수 있는 비효율성은 다음과 같다:
* 힙 할당: 모든 객체는 기본적으로 힙에 할당된다. '범프 할당'(bump allocation)을 사용하는 최신 JVM에서는 할당 자체는 빠르지만, 가비지 컬렉션 호출로 인한 성능 영향이 있을 수 있다. 최신 JIT 컴파일러는 이스케이프 분석(escape analysis)을 통해 일부 객체를 스택에 할당하여 이 문제를 완화한다.
* 내부 API 사용: 고성능이 필수적인 데이터베이스 시스템이나 메시징 라이브러리 같은 프로젝트에서는 `sun.misc.Unsafe`와 같은 내부 비공식 API를 사용하여 수동 메모리 관리나 스택 할당을 수행하기도 했다. 이는 사실상 포인터를 사용하는 것과 유사하다.
* 가상 메소드: 자바의 메소드는 기본적으로 가상 함수이다 (`final` 키워드로 변경 가능). 가상 함수 호출은 가상 메소드 테이블을 참조해야 하므로 일반 함수 호출보다 느리고 추가 메모리를 사용한다. JIT 컴파일러는 작은 함수들을 가상 함수가 아닌 일반 함수 호출로 변경하는 최적화를 수행한다.
* 런타임 캐스팅: 표준 컨테이너 사용 시 실행 시간 타입 캐스팅이 필요한 경우가 있어 성능 저하를 유발할 수 있다. 그러나 이러한 캐스팅의 상당수는 JIT 컴파일러에 의해 정적으로 제거될 수 있다.
* 안전성 검사: 자바는 메모리 안전성을 위해 배열 접근 시 경계 검사(bounds checking) 등을 수행한다. 이는 실행 시간 비용을 발생시키지만, 대부분의 JIT 컴파일러는 이러한 검사를 제거하거나 루프 밖으로 이동시키는 최적화를 시도한다. (C++에서도 필요시 범위 검사를 활성화할 수 있으며, 이 경우 컴파일러가 유사한 최적화를 수행한다.)
* 로우레벨 접근 제한: 저수준 하드웨어 제어가 불가능하므로, 컴파일러가 수행하지 못하는 특정 최적화를 개발자가 직접 구현하기 어렵다.
* 참조 의미론: 사용자 정의 타입이 항상 참조(reference) 방식으로 다루어지기 때문에, 데이터가 메모리 상에 흩어져 캐시 스레싱(cache thrashing)을 유발할 수 있다. 이는 캐시 효율성을 고려한 데이터 구조 설계를 어렵게 만들어 성능 최적화에 제약을 준다. 캐시 인식 또는 캐시 무관 데이터 구조를 통한 캐시 최적화는 성능을 크게 향상시킬 수 있는 중요한 기법인데, 자바의 참조 의미론은 이러한 최적화 구현을 어렵게 만든다.
* [[쓰레기 수집 (컴퓨터 과학)|가비지 컬렉션]]: 자동 메모리 관리는 편리하지만, 가비지 컬렉션 과정 자체가 CPU 자원을 소모하고 일시적인 프로그램 중단을 유발할 수 있으며, 메모리 오버헤드를 발생시킨다.
반면, 자바의 설계는 다음과 같은 성능상의 이점을 제공하거나 이론적으로 제시되기도 한다:
* 가비지 컬렉션과 캐시 일관성: 자바의 가비지 컬렉션은 새로운 객체를 할당할 때 연속된 메모리 공간에 배치하려는 경향이 있어, C++의 `malloc`/`new` 방식보다 캐시 일관성 측면에서 유리할 수 있다는 주장이 있다. 하지만 두 방식 모두 시간이 지남에 따라 힙 단편화를 일으켜 캐시 지역성을 해칠 수 있다는 반론도 있다. (C++에서는 작은 객체는 스택에 할당하거나, 여러 객체를 STL 컨테이너 등을 이용해 블록 단위로 할당하는 경우가 많다.)
* 런타임 최적화: JIT 컴파일러는 프로그램이 실제로 실행되는 환경(특정 CPU 모델 등)의 정보를 활용하여 코드를 최적화할 수 있다. 예를 들어, 자주 호출되는 코드 경로(핫스팟)를 집중적으로 최적화하거나, 특정 CPU의 확장 명령어셋을 활용할 수 있다. 그러나 최신 네이티브 컴파일러(C++, C 등) 역시 다양한 코드 경로를 생성하여 실행 환경에 맞는 최적화를 수행하며, 특정 아키텍처에 대한 최적화는 네이티브 컴파일러가 더 잘 수행할 수 있다는 주장도 있다.
* 공격적인 인라이닝: JIT 컴파일러는 런타임에 클래스 로딩 정보를 알 수 있으므로, 정적 컴파일러보다 더 공격적으로 가상 함수를 인라이닝할 수 있다. 이는 특히 다형성을 많이 사용하는 객체 지향 코드에서 성능 향상에 크게 기여할 수 있다. 인라이닝은 루프 벡터화나 루프 언롤링 같은 다른 최적화를 가능하게 한다.
* [[스레드]] 동기화: 자바는 언어 차원에서 스레드 동기화 메커니즘(`synchronized` 키워드 등)을 내장하고 있다. JIT 컴파일러는 이스케이프 분석 등을 통해 불필요한 락(lock)을 제거하는 최적화(lock elision)를 수행하여 멀티스레드 코드의 성능을 향상시킬 수 있다.
C++ 역시 성능과 관련된 몇 가지 고려 사항이 있다:
* [[포인터 에일리어싱]]: 포인터가 메모리의 어떤 위치든 가리킬 수 있다는 점(aliasing)은 컴파일러의 최적화를 방해하는 요인이 될 수 있다. 그러나 C++ 표준의 `strict-aliasing` 규칙은 컴파일러가 타입 정보를 기반으로 에일리어싱 가능성을 판단하여 더 적극적인 최적화를 수행할 수 있도록 돕는다. (C99 표준에서는 `restrict` 키워드를 통해 이를 명시적으로 제어할 수도 있다.)
* 템플릿 코드 블로트: 템플릿은 코드 재사용성을 높이지만, 각기 다른 타입에 대해 별도의 코드를 생성하기 때문에 과도하게 사용하면 최종 실행 파일의 크기가 커지는 문제(코드 블로트)가 발생할 수 있다. 그러나 함수 템플릿의 경우, 인라이닝이 적극적으로 이루어져 오히려 코드 크기가 줄어드는 경우도 있으며, 컴파일 타임에 타입 정보가 확정되므로 컴파일러가 더 효과적인 정적 분석과 최적화를 수행할 수 있다. 이는 런타임에 타입 소거(type erasure)를 사용하는 자바의 제네릭스보다 성능 면에서 유리할 수 있다.
* 동적 링킹과 최적화: 전통적인 C++ 컴파일러는 각 소스 파일을 개별적으로 컴파일한 후 링커를 통해 합치므로, 다른 동적 라이브러리나 모듈에 있는 함수를 호출하는 경우 인라이닝과 같은 모듈 간 최적화가 어려웠다. 하지만 최신 컴파일러(MSVC, Clang+LLVM 등)는 링크 타임 최적화(Link-Time Optimization, LTO) 또는 링크 타임 코드 생성(Link-Time Code Generation, LTCG) 기능을 지원하여, 링크 단계에서 프로그램 전체를 분석하고 모듈 경계를 넘나드는 최적화를 수행할 수 있다.
* 스레드 최적화: 과거 C++는 스레드 지원을 주로 운영체제나 외부 라이브러리에 의존했기 때문에, 컴파일러가 스레드 관련 최적화를 수행하기 어려웠다. 하지만 C++11 표준부터 언어 차원의 메모리 모델과 스레드 지원 라이브러리가 도입되어, 현대 컴파일러는 멀티스레드 환경을 고려한 최적화를 수행할 수 있게 되었다.
일반적으로 자바는 메모리 할당이나 파일 입출력(I/O)과 같은 작업에서는 C++와 비슷하거나 더 나은 성능을 보일 수 있지만, 순수한 산술 연산이나 삼각 함수 계산 등 CPU 집약적인 작업에서는 C++가 더 빠른 경향이 있다. 수치 연산 성능은 자바의 새로운 버전에서 많이 개선되었지만, 플랫폼 간 부동소수점 연산 결과의 일관성을 보장하기 위한 오버헤드 등으로 인해, 아직도 C++나 포트란에 비해 느리다는 평가가 있다.