기본형 8가지와 참조형
Java의 자료형은 크게 기본형(primitive type) 과 참조형(reference type) 으로 나뉩니다.
byte, short, int, long / 실수형 float, double / 문자형 char / 논리형 boolean, 총 8가지.String, 배열, 클래스로 만든 객체가 여기에 속하며 변수에는 객체의 주소(참조) 가 저장됩니다.| 분류 | 자료형 | 저장 데이터 | 크기 |
|---|---|---|---|
| 정수형 | byte | 작은 정수 (-128 ~ 127) | 1바이트 |
| 정수형 | short | 중간 정수 (-32,768 ~ 32,767) | 2바이트 |
| 정수형 | int | 정수 (가장 기본, 약 -21억 ~ +21억) | 4바이트 |
| 정수형 | long | 큰 정수 (int 범위의 약 43억 배) | 8바이트 |
| 실수형 | float | 소수점이 있는 수 | 4바이트 |
| 실수형 | double | 소수점이 있는 수 (더 정밀) | 8바이트 |
| 문자형 | char | 문자 하나 | 2바이트 |
| 논리형 | boolean | 참(true) / 거짓(false) | 1바이트 |
int age = 25; // 정수 double height = 175.5; // 소수점이 있는 수 char grade = 'A'; // 문자 하나 boolean pass = true; // 참/거짓 long big = 3000000000L; // int 범위(약 21억) 초과 시 반드시 L을 붙임
기본형 vs 참조형: 대입 시 무엇이 복사되나
int y = x는 x의 값을 y에 복사하므로 이후 x를 바꿔도 y는 그대로입니다.Student s2 = s1은 두 변수가 같은 객체를 가리키게 되어, 한쪽에서 객체를 수정하면 다른 쪽에도 보입니다.이 규칙은 객체 배열, 메서드 매개변수 전달 문제에서 반복 출제됩니다.
자동 형변환 (묵시적)
좁은 타입에서 넓은 타입으로는 자동 변환되며 데이터 손실이 없습니다. 순서는 다음과 같습니다(범위 기준이라 8바이트 long이 4바이트 float보다 앞).
byte → short → int → long → float → double // ↑ char도 int 단계에서 합류 int a = 10; double b = a; // int → double 자동. b = 10.0 char c = 'A'; int num = c; // char → int 자동. num = 65
강제 형변환 (명시적)
넓은 타입에서 좁은 타입으로는 (자료형)을 직접 써야 하며 데이터 손실이 생길 수 있습니다.
double pi = 3.14; int n1 = (int) pi; // 3 (반올림 아님, 소수점 이하 버림) int n2 = (int) 3.99; // 3 char ch = (char) 65; // 'A'
| 변환 | 방법 | 예시 | 결과 |
|---|---|---|---|
| int → double | 자동 | double b = 10; | 10.0 |
| double → int | 강제 | int a = (int) 3.14; | 3 |
| char → int | 자동 | int n = 'A'; | 65 |
| int → char | 강제 | char c = (char) 65; | 'A' |
문자열 결합 연산자 +
+는 양쪽이 모두 숫자면 덧셈, 한쪽이라도 문자열이면 나머지도 문자열로 변환해 이어붙입니다. 계산은 왼쪽에서 오른쪽으로 진행됩니다.
System.out.println(1 + 2); // 3 (숫자 + 숫자) System.out.println("A" + 1); // A1 (문자열 + 숫자) System.out.println(1 + 2 + "감자"); // 3감자 (1+2=3 먼저, 그 뒤 "3"+"감자") System.out.println("감자" + 1 + 2); // 감자12 (좌결합: "감자1" → "감자12") System.out.println("감자" + (1 + 2)); // 감자3 (괄호가 먼저)
char 산술
char끼리 또는 char와 숫자를 +/-로 계산하면 Java가 char를 int로 자동 변환한 뒤 계산하므로 결과는 항상 int입니다.
int diff = 'C' - 'A'; // 67 - 65 = 2 (char 'B'가 아니라 int 2) int sum = 'A' + 1; // 65 + 1 = 66 System.out.println(1 + '3'); // 52 (둘 다 숫자: 1 + 51) System.out.println("1" + '3'); // 13 (문자열이 앞: 글자 그대로) System.out.println("A" + 'B' + 1); // AB1 (("A"+'B')="AB", 그 뒤 +1)
함정: 앞에 문자열이 먼저 등장하면 그 뒤 char는 숫자 변환 없이 글자 그대로 붙습니다. 좌결합 계산 중 "어느 단계부터 String이 되었는가"가 핵심입니다.
삼항 연산자
조건식 ? true일 때 값 : false일 때 값 형태로, if-else를 한 줄의 값 선택식으로 줄인 문법입니다. 조건식은 반드시 boolean 결과여야 합니다(C와 달리 1/0을 조건으로 쓸 수 없음).
int score = 75; String result = score >= 60 ? "P" : "F"; // "P"
중첩 삼항은 오른쪽부터 묶어 읽습니다.
int n = 5; String r = n > 5 ? "A" : n == 5 ? "B" : "C"; // 해석: n > 5 ? "A" : (n == 5 ? "B" : "C") // n>5 는 false → (n==5 ? "B" : "C") → n==5 true → "B"
?:는 우선순위가 낮아 비교·논리 연산자가 먼저 계산됩니다: score >= 80 && submitted ? "A" : "B" 는 ((score>=80) && submitted) ? "A" : "B".boolean 조건 (C와의 차이)
Java의 조건식은 반드시 true/false가 나오는 식이어야 합니다. C처럼 정수 1/0을 조건으로 직접 쓸 수 없습니다.
int score = 80; boolean pass = score >= 60; // true if (pass) { System.out.println("합격"); }
for-each문 (향상된 for문)
배열의 원소를 처음부터 끝까지 하나씩 꺼내는 반복문입니다. 콜론(:)은 "~에서 꺼낸다"는 뜻입니다.
int[] arr = {10, 20, 30}; for (int x : arr) { // "arr에서 원소를 하나씩 꺼내 x에 담아라" System.out.println(x); } // 출력: 10 / 20 / 30 (각 줄)
일반 for문 vs for-each문
| 구분 | 일반 for문 | for-each문 |
|---|---|---|
| 인덱스 사용 | i로 접근 가능 | 인덱스 없음 |
| 값 접근 | arr[i] | 변수에 바로 담김 |
| 역순 순회 | 가능 (i--) | 불가능 (항상 처음부터 끝) |
| 값 수정 | arr[i] = 값 으로 가능 | 배열 원본 수정 불가 |
함정: for-each의 변수는 배열에서 꺼낸 복사본입니다. 그 변수를 바꿔도 원본 배열은 그대로입니다.
int[] arr = {1, 2, 3}; for (int x : arr) { x = x * 10; // x만 바뀜, arr는 그대로 } // arr는 여전히 {1, 2, 3} // 원본을 바꾸려면 인덱스로 접근하는 일반 for문 사용 for (int i = 0; i < arr.length; i++) { arr[i] = arr[i] * 10; // arr가 {10, 20, 30}으로 바뀜 }
배열의 기본값
배열을 생성하면 모든 칸이 타입별 기본값으로 자동 초기화됩니다.
| 배열 타입 | 기본값 |
|---|---|
| int / long 등 정수형 | 0 |
| double | 0.0 |
| boolean | false |
| String / 객체 배열 | null |
boolean[] visited = new boolean[10]; // 전부 false int[] counts = new int[5]; // 전부 0
char는 내부적으로 정수이므로 배열 인덱스로 쓸 수 있습니다. 'a'는 97이므로 arr['a']는 arr[97]과 같으며, 문자 등장 여부 기록 문제에서 자주 활용됩니다.
boolean[] seen = new boolean[256]; char c = 'a'; seen[c] = true; // seen[97] = true
2차원 배열은 "배열의 배열"
int[][] matrix는 각 원소가 또 하나의 1차원 배열(행)입니다. for-each로 바깥에서 꺼내지는 원소는 int[](행)입니다.
int[][] matrix = { {1, 2, 3}, {4, 5, 6} }; for (int[] row : matrix) { // row = 각 행 (1차원 배열) for (int value : row) { // value = 행 안의 각 값 System.out.print(value + " "); } System.out.println(); }
실행 결과:
1 2 3 4 5 6
객체 배열과 참조 교환 (빈출 함정)
객체 배열의 각 요소는 객체의 주소(참조) 를 저장합니다. BO[] arr = {a, b, c}는 a, b, c에 든 주소를 배열에 복사하므로, 배열 요소와 원본 변수가 같은 객체를 가리킵니다.
class BO { int v; BO(int v) { this.v = v; } }
BO a = new BO(1), b = new BO(2), c = new BO(3);
BO[] arr = {a, b, c};
// (1) 참조 교환: 배열 안의 주소만 바뀜
BO t = arr[0];
arr[0] = arr[2];
arr[2] = t;
// a, b, c가 가리키는 대상은 그대로 → a.v=1, b.v=2, c.v=3
// (2) 필드 수정: 주소를 따라가 객체 자체를 변경 → 원본도 바뀜
arr[1].v = 99; // arr[1]과 b는 같은 객체 → b.v도 99
| 동작 | 설명 | 원본 변수 영향 |
|---|---|---|
arr[0] = arr[2] | 배열 안의 참조를 교환 | 없음 (a, b, c 그대로) |
arr[1].v = 99 | 요소가 가리키는 객체의 필드 수정 | 있음 (원본 객체 변경) |
시험 팁: 배열 요소 교환 후a.v,b.v,c.v를 물으면 원본 변수 기준으로 답하세요. 바뀌는 것은 배열 안의 참조뿐입니다.
String은 불변(immutable)
한 번 만든 문자열 객체의 내용은 바뀌지 않습니다. str = str + "A"처럼 보여도 실제로는 새 객체를 만들어 참조만 갈아끼우는 것입니다.
String s = "Hello"; s = s + " World"; // "Hello World"라는 새 객체 생성 → s가 새 객체를 가리킴 // 원래 "Hello" 객체는 그대로 남았다가 GC가 회수
반복 연결은 매 반복마다 새 String 객체가 생겨 비용이 큽니다.
String result = ""; for (int i = 0; i < 1000; i++) { result += "x"; // 매 반복마다 새 String (총 약 1000개 생성) }
StringBuilder는 가변(mutable)
append()로 덧붙여도 새 객체가 생기지 않고 같은 객체의 내부 배열에 누적됩니다.
StringBuilder sb = new StringBuilder(); for (int i = 0; i < 1000; i++) { sb.append("x"); // 같은 sb 객체에 누적 } String result = sb.toString(); // 마지막에 한 번만 String으로 변환
참조 동일성 차이 (== 비교)
// StringBuilder — 같은 객체 StringBuilder sb1 = new StringBuilder("abc"); StringBuilder sb2 = sb1; sb1.append("def"); System.out.println(sb1 == sb2); // true (같은 객체) System.out.println(sb2.toString()); // "abcdef" (sb2도 같이 변함) // String — 새 객체 String s1 = "abc"; String s2 = s1; s1 = s1 + "def"; System.out.println(s1 == s2); // false (s1은 새 객체) System.out.println(s2); // "abc" (s2는 그대로)
==는 같은 주소를 가리키는지 비교합니다. 내용 비교는equals()로 따로 해야 합니다.
StringBuilder 주요 메서드 (시작값 "Hello" 기준 단독 호출)
| 메서드 | 동작 | 예시 | 결과 |
|---|---|---|---|
| append(x) | 끝에 덧붙임 | sb.append("W") | "HelloW" |
| insert(i, x) | i번 자리에 삽입 | sb.insert(0, "A") | "AHello" |
| delete(a, b) | a ~ b-1 범위 삭제 | sb.delete(1, 2) | "Hllo" |
| reverse() | 뒤집기 | sb.reverse() | "olleH" |
| length() | 현재 길이 | sb.length() | 5 |
| toString() | String으로 변환 | sb.toString() | "Hello" |
기본 제공 클래스: Integer, Math
Integer, Math, String, System은 import 없이 바로 쓸 수 있습니다.
int a = Integer.valueOf("5"); // 문자열 → 정수 5 int b = Integer.parseInt("42"); // 문자열 → 정수 42 (valueOf와 동일하게 이해)
| 메서드 | 하는 일 | 예시 | 결과 |
|---|---|---|---|
| Math.max(a, b) | 큰 값 | Math.max(3, 7) | 7 |
| Math.min(a, b) | 작은 값 | Math.min(3, 7) | 3 |
| Math.abs(n) | 절댓값 | Math.abs(-5) | 5 |
| Math.pow(a, b) | a의 b제곱 (double) | Math.pow(2, 3) | 8.0 |
| Math.round(n) | 반올림 (long) | Math.round(3.6) | 4 |
출력 함수와 서식 지정자
System.out.print(): 줄바꿈 없음 / System.out.println(): 줄바꿈 함.printf()는 서식 지정자로 형식을 지정하며 줄바꿈하지 않습니다.System.out.printf("%s는 %d살입니다", "감자", 3); // 감자는 3살입니다 System.out.printf("%5d", 42); // " 42" (5칸 확보, 오른쪽 정렬) System.out.printf("%-5d", 42); // "42 " (왼쪽 정렬) System.out.printf("%.2f", 3.14);// "3.14" System.out.printf("%f", 3.14); // "3.140000" (기본 소수점 6자리)
| 서식 지정자 | 의미 |
|---|---|
| %d | 정수 |
| %s | 문자열 |
| %f | 실수 (기본 소수점 6자리) |
| %c | 문자 1개 |
| %% | % 기호 자체 |
클래스 vs 객체(인스턴스)
new로 생성된 실체(붕어빵). 변수에는 객체가 통째로가 아니라 주소(참조) 가 저장됩니다.class Student {
String name; // 속성(필드)
String dream;
void study() { // 메서드
System.out.println(name + "이(가) 공부합니다");
}
}
Student s1 = new Student(); // 설계도로 실체 생성, s1에는 주소가 저장됨
s1.name = "감자";
s1.study(); // 감자이(가) 공부합니다
생성자 (Constructor)
객체 생성 시 자동 호출되어 초기화하는 특별한 메서드입니다. 클래스명과 동일하고 반환 타입이 없습니다.
class Student {
String name;
String dream;
Student(String name, String dream) { // 생성자
this.name = name; // this.name = 필드, name = 매개변수
this.dream = dream;
}
}
Student s = new Student("감자", "개발자");
기본 생성자 함정: 생성자를 하나도 정의하지 않으면 컴파일러가 매개변수 없는 기본 생성자Student() {}를 자동 생성합니다. 하지만 매개변수 생성자를 직접 정의하면 기본 생성자는 자동 생성되지 않으므로new Student()는 컴파일 에러가 됩니다.
this()로 다른 생성자 호출 (생성자 체이닝)
class Student {
String name; String dream;
Student() {
this("이름없음", "미정"); // 아래 생성자 호출 (반드시 첫 줄)
}
Student(String name, String dream) {
this.name = name;
this.dream = dream;
}
}
this()는 생성자의 첫 줄에만 올 수 있습니다.this() 또는 super() 중 하나만 올 수 있습니다.this()(다른 생성자 호출)와 this.필드(이 객체의 필드 접근)는 다른 문법입니다.접근제어자
| 접근제어자 | 같은 클래스 | 같은 패키지 | 자식 클래스 | 외부 전체 |
|---|---|---|---|---|
| public | O | O | O | O |
| protected | O | O | O | X |
| default (생략) | O | O | X | X |
| private | O | X | X | X |
접근 범위: public > protected > default(생략) > private
캡슐화 패턴(빈출): 필드는private으로 감추고publicgetter/setter로만 접근하게 하면 잘못된 값 유입을 막을 수 있습니다.
class Student {
private int score; // 외부 직접 접근 차단
public void setScore(int s) {
if (s >= 0 && s <= 100) score = s; // 유효성 검사
}
public int getScore() { return score; }
}
public을 붙여야 합니다(생략하면 접근 범위가 좁아져 컴파일 에러).private으로 선언하면 외부에서 new로 객체 생성이 차단됩니다.default는 키워드가 아니라 "아무것도 안 씀"입니다(switch의 default와 다름).static이란
static은 "클래스에 소속된"이라는 의미입니다. 객체(인스턴스)를 만들지 않아도 클래스명으로 바로 사용할 수 있습니다.
class Gamja {
static int count = 0; // 클래스 변수: 모든 객체가 공유
static String check(int num) { // 클래스 메서드
return (num >= 0) ? "positive" : "negative";
}
String name; // 인스턴스 변수: 객체마다 별도
}
Gamja.check(1); // static: 객체 없이 클래스명으로 호출
시험 핵심:클래스명.메서드()형태로 호출한다면 그 메서드에는 반드시static이 필요합니다.
static 변수는 모든 인스턴스가 공유
Gamja a = new Gamja(); Gamja b = new Gamja(); a.increase(); // count = 1 b.increase(); // count = 2 (a와 b가 같은 count 공유) System.out.println(Gamja.count); // 2
static 메서드의 제약
static 메서드 안에서는 인스턴스 변수·인스턴스 메서드에 직접 접근할 수 없습니다(객체 없이 호출될 수 있으므로). main이 항상 static인 이유도 프로그램 시작 시 아직 객체가 없기 때문입니다.
| static 메서드에서 | 접근 가능 여부 |
|---|---|
| static 변수 / static 메서드 | 가능 |
| 인스턴스 변수 / 인스턴스 메서드 | 불가능 |
상속과 초기화 순서 (빈출 함정)
자식 객체 생성 시 부모 static → 자식 static → 부모 인스턴스 초기화 블록 → 부모 생성자 → 자식 인스턴스 초기화 블록 → 자식 생성자 순서로 실행됩니다. static 블록은 클래스 로드 시 딱 한 번만 실행됩니다.
class Parent {
static { System.out.print("1 "); } // 부모 static
{ System.out.print("2 "); } // 부모 인스턴스 초기화 블록
Parent() { System.out.print("3 "); } // 부모 생성자
}
class Child extends Parent {
static { System.out.print("4 "); }
{ System.out.print("5 "); }
Child() { System.out.print("6 "); }
}
public class Main {
public static void main(String[] args) {
new Child();
System.out.println();
new Child(); // 두 번째: static은 이미 실행됨 → 건너뜀
}
}
실행 결과:
1 4 2 3 5 6 2 3 5 6
세 규칙: (1) static이 먼저, 인스턴스가 나중 (2) 부모가 먼저, 자식이 나중 (3) static은 한 번만, 인스턴스는 매번.
extends와 super
extends로 부모의 필드·메서드를 물려받습니다. super(...)는 부모 생성자 호출, super.x는 부모의 필드·메서드 접근입니다. super()는 생성자 첫 줄에 위치해야 합니다.
class Rectangle {
int width, height;
Rectangle(int width, int height) { // 매개변수 생성자만 있음 → 기본 생성자 없음
this.width = width; this.height = height;
}
}
class Square extends Rectangle {
Square(int s) {
super(s, s); // 부모에 기본 생성자가 없으므로 반드시 명시해야 함
}
}
| 부모 클래스 상태 | super 필요 여부 |
|---|---|
| 생성자 없음(기본 생성자 자동 생성) | 생략 가능(컴파일러가 super() 자동 호출) |
| 기본 생성자 있음 | 생략 가능 |
| 매개변수 생성자만 있음 | 반드시 명시 |
extends vs implements
| 키워드 | 사용 대상 | 의미 |
|---|---|---|
| extends | 클래스에서 클래스로 | 상속 |
| implements | 클래스에서 인터페이스로 | 구현 |
오버라이딩(Overriding)
부모 메서드를 자식이 같은 이름·같은 매개변수·같은 반환 타입으로 재정의하는 것. 인스턴스 메서드는 실제 객체 타입 기준(동적 바인딩) 으로 호출됩니다.
class Animal { String sound() { return "..."; } }
class Dog extends Animal { String sound() { return "멍멍"; } }
class Cat extends Animal { String sound() { return "야옹"; } }
Animal a = new Dog();
System.out.println(a.sound()); // 멍멍 (실제 객체 Dog 기준)
오버라이딩 vs 오버로딩
| 구분 | 오버라이딩(Overriding) | 오버로딩(Overloading) |
|---|---|---|
| 의미 | 부모 메서드를 자식이 재정의 | 같은 이름 메서드를 여러 개 정의 |
| 메서드 이름 | 같음 | 같음 |
| 매개변수 | 같음 | 다름(타입 또는 개수) |
| 클래스 관계 | 부모-자식 간 | 같은 클래스 내에서도 가능 |
| 결정 시점 | 런타임(동적 바인딩) | 컴파일타임(정적 바인딩) |
오버로딩: 인자 타입으로 메서드 선택
static int calc(int value) { return value * 2; }
static int calc(String str) { return Integer.valueOf(str) * 3; }
calc(5); // calc(int) 호출 → 10
calc("5"); // calc(String) 호출 → 15
오버로딩 + 재귀 전환(빈출):calc(String)으로 진입 후Integer.valueOf()로 int로 바꾸면, 이후calc(value - 1)은 int 인자이므로calc(int)로 전환됩니다.calc(String)은 처음 한 번만 실행됩니다.
핵심 함정: 필드·static 메서드는 선언 타입 기준
Parent ref = new Child()에서 인스턴스 메서드만 실제 객체(Child) 기준으로 호출되고, 필드와 static 메서드는 선언 타입(Parent) 기준입니다.
class Parent {
String x = "Parent";
int m(int i) { return i + 2; } // 인스턴스 메서드
public static String id() { return "P"; } // static 메서드
}
class Child extends Parent {
String x = "Child"; // 필드 하이딩
int m(int i) { return i + 3; } // 오버라이딩
public static String id() { return "C"; } // 별개의 static 메서드
}
Parent ref = new Child();
System.out.println(ref.m(2)); // 5 (인스턴스 메서드 → 실제 객체 Child)
System.out.println(ref.x); // Parent (필드 → 선언 타입 Parent)
System.out.println(ref.id()); // P (static → 선언 타입 Parent로 치환)
| 구분 | 소속 | 바인딩 시점 | 기준 |
|---|---|---|---|
| 인스턴스 메서드 | 객체 | 런타임(동적) | 실제 객체 → 오버라이딩 적용 |
| 필드 | 클래스 | 컴파일(정적) | 선언 타입 → 필드 하이딩 |
| static 메서드 | 클래스 | 컴파일(정적) | 선언 타입 |
컴파일타임 바인딩: 오버로드 선택은 선언 타입 시야에서 고정
오버로드 선택은 호출 지점의 선언 타입 + 인자 타입으로 컴파일 시점에 고정되고, 그 뒤 런타임에 실제 구현(오버라이딩 결과)이 실행됩니다.
class A { String f(Object x) { return "1"; } }
class B extends A {
String f(Object x) { return "2"; } // A의 f(Object) 오버라이딩
String f(String x) { return "3"; } // B에만 있는 오버로드
}
A a = new B();
System.out.println(a.f("hello")); // 2
풀이: a의 선언 타입은 A 이므로 컴파일러는 A의 f(Object) 하나만 보고 f(Object) 호출로 고정합니다. 런타임에 실제 객체 B의 오버라이딩 구현 f(Object)가 실행되어 "2". B에만 있는 f(String)은 선언 타입 A의 시야 밖이라 선택되지 않습니다.
추상 클래스(abstract class)
abstract가 붙은, 직접 객체를 생성할 수 없는 클래스입니다. 추상 메서드(선언만 있고 본문 없이 ;로 끝남)와 일반 메서드·필드를 함께 가질 수 있습니다.
abstract class Vehicle {
String name;
Vehicle(String name) { this.name = name; }
abstract String sound(); // 추상 메서드 (자식이 구현)
String describe() { return name + " 소리: " + sound(); } // 일반 메서드
}
class Car extends Vehicle {
Car(String name) { super(name); }
String sound() { return "부릉부릉"; }
}
class Bike extends Vehicle {
Bike(String name) { super(name); }
String sound() { return "따르릉"; }
}
Vehicle v1 = new Car("스파크");
System.out.println(v1.describe()); // 스파크 소리: 부릉부릉
Vehicle v2 = new Bike("자전거");
System.out.println(v2.describe()); // 자전거 소리: 따르릉
다형성 함정: 부모의 일반 메서드describe()가 부른 추상 메서드sound()도 실제 객체(자식)의 구현이 실행됩니다. 전체 흐름은 부모가 정하고 달라지는 부분만 자식이 채우는 구조가 다형성입니다.
추상 메서드 vs 일반 메서드
abstract가 붙은 것만 추상 메서드입니다. 추상 클래스 안이라도 abstract가 없으면 일반 메서드이며, 자식이 오버라이딩하지 않으면 부모 것이 그대로 실행됩니다.getName()(부모), getName(String)(자식), getName(byte[])(자식)는 각각 별개입니다.인터페이스(interface)
클래스가 반드시 구현해야 할 메서드 목록을 정의한 약속(계약) 입니다. implements로 구현하며, 선언된 모든 메서드를 구현해야 하고 구현 메서드에는 public을 붙여야 합니다.
interface Person { void sayHello(); } // public 생략해도 자동 public
class OliveYoung implements Person {
public void sayHello() { // 반드시 public
System.out.println("어서오세요 올리브영입니다~");
}
}
class Starbucks implements Person {
public void sayHello() {
System.out.println("주문하신 아메리카노 나왔습니다~");
}
}
Person p1 = new OliveYoung(); // 다형성: 인터페이스 타입에 구현 객체
Person p2 = new Starbucks();
p1.sayHello(); // 어서오세요 올리브영입니다~
p2.sayHello(); // 주문하신 아메리카노 나왔습니다~
인터페이스는 여러 개 동시에 구현할 수 있습니다(쉼표로 구분). extends(클래스 상속)와 implements를 함께 쓸 때는 extends가 먼저 옵니다.
class Duck implements Walkable, Swimmable { /* walk(), swim() 모두 구현 */ }
class Dog extends Animal implements Walkable { /* ... */ }
추상 클래스 vs 인터페이스
| 구분 | 추상 클래스 | 인터페이스 |
|---|---|---|
| 키워드 | abstract class | interface |
| 상속/구현 | extends | implements |
| 일반 메서드 | 가질 수 있음 | 기본적으로 불가(추상 메서드만) |
| 필드(변수) | 가질 수 있음 | 상수만 가능 |
| 다중 상속/구현 | 불가(하나만 상속) | 가능(여러 개 구현) |
정처기 실기에서는 "인터페이스 = 추상 메서드만"으로 이해하면 충분합니다(Java 8+ default 메서드는 기출 없음).
기본 구조
try { int result = 10 / 0; // 예외 발생! 이 줄 이후는 건너뜀 System.out.print("성공"); // 실행 안 됨 } catch (Exception e) { System.out.print("실패"); // 실행됨 } // 출력: 실패
| 키워드 | 역할 |
|---|---|
| try | 예외가 발생할 수 있는 코드를 감싸는 블록 |
| catch | 예외 발생 시 실행되는 블록 |
| finally | 예외 발생 여부와 관계없이 항상 실행 |
| throw | 예외를 직접 발생시킴 (메서드 본문 안) |
| throws | 메서드가 예외를 던질 수 있음을 선언 (메서드 선언부) |
finally: 항상 실행 (빈출 함정)
try나 catch에서 return을 만나도 finally는 return 직전에 실행됩니다.
static String test() {
try {
System.out.print("A");
return "try"; // 반환값 "try"를 정해둠
} catch (Exception e) {
System.out.print("B");
return "catch";
} finally {
System.out.print("C"); // return 전에 실행됨
}
}
// 출력: AC, 반환값: "try"
더 큰 함정: finally에 return이 있으면 try/catch의 반환값을 덮어씁니다.static int test2() {
try { return 1; } // 1을 정해둠
finally { return 2; } // finally의 return이 최종 → 1은 버려짐
}
// 반환값: 2
다중 catch와 예외 계층
catch는 위에서 아래로 검사하여 처음 일치하는 하나만 실행됩니다. 모든 예외의 최상위는 Throwable이고, 그 아래 Exception/Error, Exception 아래 RuntimeException·IOException 등으로 갈라집니다.
Throwable └── Exception ├── RuntimeException │ ├── ArithmeticException │ ├── ArrayIndexOutOfBoundsException │ ├── NullPointerException │ └── ClassCastException └── IOException └── FileNotFoundException
순서 함정: 구체적 예외를 먼저, 넓은 예외(Exception)를 나중에 써야 합니다.Exception을 먼저 쓰면 아래 구체적 catch는 도달 불가라 컴파일 에러가 납니다.
// 올바른 순서 try { int result = 10 / 0; } catch (ArithmeticException e) { // 구체적 예외 먼저 System.out.print("산술"); } catch (Exception e) { // 넓은 예외 나중 System.out.print("기타"); } // 출력: 산술
| 예외 | 발생 상황 |
|---|---|
| ArithmeticException | 0으로 나눌 때 (10 / 0) |
| ArrayIndexOutOfBoundsException | 배열 범위 초과 |
| NullPointerException | null에 접근할 때 |
| ClassCastException | 잘못된 타입 변환 |
| NumberFormatException | 숫자 변환 실패 (Integer.valueOf("abc")) |
throw / throws
public static int check(int x) {
try {
if (x > 2) throw new Exception(); // throw: 예외 직접 발생
return x * 2;
} catch (Exception e) {
return 7;
}
}
// check(1) → 2, check(3) → 7
throw: 예외를 발생시킴(동사 원형, 메서드 본문 안).throws: 메서드가 예외를 던질 수 있음을 선언(메서드 선언부). throws가 붙은 메서드를 호출하는 쪽은 try-catch로 감싸거나 자신도 throws를 선언해야 합니다.정처기 실기 코딩 문제는 대부분 "출력/반환 결과 맞히기"입니다. 아래 함정들이 반복 출제됩니다.
선언 타입 vs 실제 객체 (Parent ref = new Child())
+ 연산의 좌결합과 타입 전환
1 + 2 + "감자" 는 3감자, "감자" + 1 + 2 는 감자12.'C' - 'A' 는 2. 단 앞에 문자열이 오면 글자 그대로 붙음.finally
객체 배열
arr[0]=arr[2])은 원본 변수(a,b,c)에 영향 없음.arr[1].v=10)은 원본 객체 변경.for-each
static 초기화 순서
오버로드 선택은 컴파일 시점 선언 타입 기준
강제 형변환은 버림
(int) 3.99 는 3 (반올림 아님). 반올림은 Math.round.