자주 보지 않으면 잊게되는 Java 기초 정리

매번 공부해도 너무 당연하다는 듯이 넘어가 잊게되거나, 기억해 두면 좋을것 같은 기초적인 문법과 개념들을 간단히 정리해보겠습니다.

변수

덧셈 연산자(+)는 피연산자가 모두 숫자일 때는 두 수를 더하지만, 피연산자 중 어느 한쪽이 String이면 나머지 한 쪽을 먼저 String으로 변환한 다음 두 String을 결합한다.

참조변수의 출력이나 덧셈연산자를 이용한 참조변수와 문자열의 결합에는 toString()이 자동적으로 호출되어 참조변수를 문자열로 대치한 후 처리한다.

연산자

  • x << 2 + 1 : 쉬프트연산자(<<)는 덧셈연산자보다 우선순위가 낮다. 그래서 해당 식은 ‘x << (2 + 1)’ 과 같다.
  • data & 0xFF == 0 : 논리연산자(&)는 비교연산자(==)보다 우선순위가 낮으므로 비교연산 후에 논리연산이 수행된다. 그래서 해당 식은 ‘data & (0xFF == 0)’ 과 같다.
  • x < -1 || x > 3 && x < 5 : 논리연산자 중에서 AND를 의미하는 ‘&&’가 OR을 의미하는 ‘||’보다 우선순위가 높다. 그래서 해당 식은 ‘x < -1 || (x > 3 && x < 5)’ 과 같다.

산술 변환 : 연산 전에 피연산자 타입의 일치를 위해 자동 형변환되는 것. 이 변환은 이항 연산에서 뿐만 아니라 단항 연산에서도 일어난다. ‘산술 변환’의 규칙은 다음과 같다.

  1. 두 피연산자의 타입을 같게 일치시킨다. (보다 큰 타입으로 일치)
    • long + int -> long + long -> long
    • float + int -> float + float -> float
  2. 피연산자의 타입이 int보다 작은 타입이면 int로 변환된다.
    • byte + short -> int + int -> int
    • char + short -> int + int -> int

첫 번째 규칙은 자동 형변환처럼 피연산자의 값손실을 최소화하기 위한 것이고, 두 번째 규칙은 정수형의 기본 타입인 int가 가장 효율적으로 처리할 수 있는 타입이기 때문에, 그리고 int보다 작은 타입, 예를 들면 char나 short의 표현범위가 좁아서 연산중에 오버플로우(overflow)가 발생할 가능성이 높기 때문에 만들어진 것이다.

1
2
3
4
5
6
7
8
long a = 1000000 * 1000000;
long b = 1000000 * 1000000L;
System.out.println("a="+a);
System.out.println("b="+b);

// 결과
a = -727379968
b = 1000000000000

위의 예제에서 ‘1000000 * 1000000’의 결과가 1000000000000(2*10의 12승)임에도 불구하고, -727379968라는 결과가 출력되었다. 그 이유는 int 타입과 int 타입의 연산 결과는 int 타입인데, 연산결과가 int 타입의 최대값인 1000000000(2*10의 9승)을 넘으므로 오버플로우(overflow)가 발생했기 때문이다. 이미 오버플로우가 발생한 값을 아무리 long 타입의 변수에 저장해도 소용이 없다.

1
2
3
4
5
6
7
8
9
char c1 = 'a';      // c1에는 문자 'a'의 코드값은 97이 저장된다.
char c2 = c1; // c1에 저장되어 있는 값이 c2에 저장된다.
char c3 = ' '; // c3를 공백으로 초기화 한다.

int i = c1 +1; // 'a' + 1 -> 97 + 1 -> 98

c3 = (char)(c1 + 1);
c2++;
c2++;

위의 예제에서 c2++; 대신에 c2=c2+1;을 사용하면 에러가 발생할 것이다. c2+1의 연산결과는 int형이며, 그 결과를 다시 c2에 담으려면 형변환 연산자를 사용하여 char형으로 형변환해야 하기 때문이다.

1
2
3
char c1 = 'a';
char c2 = c1 + 1; // 컴파일 에러 발생 O
char c2 = 'a' + 1; // 컴파일 에러 발생 X

‘a’ + 1은 리터럴 간의 연산이기 때문에 에러가 발생하지 않는다. 상수 또는 리터럴 간의 연산은 실행과정동안 변하는 값이 아니기 때문에, 컴파일 시에 컴파일러가 계산해서 그 결과로 대체함으로써 코드를 보다 효율적으로 만든다. 컴파일러가 미리 덧셈연산을 수행하기 때문에 실행 시에는 덧셈 연산이 수행되지 않는다. 수식에 변수가 들어가 있는 경우에는 컴파일러가 미리 계산을 할 수 없기 때문에 형변환을 해줘야한다. (char c2 = (char) (c1 + 1)) 그렇지 않으면 컴파일 에러가 발생한다.

1
2
float f = 0.1f;         // f에 0.10000000149011612로 저장된다.
double d = 0.1; // d에 0.10000000000000001로 저장된다.

float 타입의 값을 double 타입으로 형변환하면, 부호와 지수는 달라지지 않고 그저 기수의 빈자리를 0으로 채울 뿐이므로 0.1f를 double타입으로 형변환해도 그 값은 전혀 달라지지 않는다. 즉, float 타입의 값을 정밀도가 더 높은 double 타입으로 형변환했다고 해서 오차가 적어지는 것이 아니라는 말이다.

1
2
3
4
5
6
7
8
9
10
String str1 = "abc";
String str2 = new String("abc");

"abc" == "abc" ? true
str1 == "abc" ? true
str2 == "abc" ? false
str1.equals("abc") ? true
str2.equals("abc") ? true
str2.equals("ABC") ? false
str2.equalsIgnoreCase("ABC") ? true

str2와 “abc”의 내용이 같은데도 ‘==’로 비교하면, false를 결과로 얻는다. 내용은 같지만 서로 다른 객체이기 때문이다. 그러나 equals()는 객체가 달라도 내용이 같으면 true를 반환한다. 그래서 문자열을 비교할 때는 항상 equals()를 사용해야 한다.

  • 효율적인 연산
    • OR 연산 ‘||’의 경우, 두 피연산자 중 어느 한 쪽만 ‘참’이어도 전체 연산결과가 ‘참’이므로 좌측 피연산자가 ‘true(참)’이면, 우측 피연산자의 값은 평가하지 않는다.
    • AND 연산 ‘&&’의 경우, 어느 한쪽만 ‘거짓(0)’이어도 전체 연산결과가 ‘거짓(0)’이므로 좌측 피연산자가 ‘거짓(0)’이면, 우측 피연산자의 값은 평가하지 않는다.

비트 XOR 연산자 ‘^’는 두 피연산자의 비트가 다를 때만 1이 된다. 그리고 같은 값으로 두고 XOR 연산을 수행하면 원래의 값으로 돌아온다는 특징이 있어서 간단한 암호화에 사용된다.

비트 전환 연산자는 피연산자의 타입이 int보다 작으면 int로 자동 형변환(산술 변환) 후에 연산하기 때문에 연산결과는 32자리의 2진수이다.

쉬프트 연산자의 좌측 피연산자는 산술변환이 적용되어 int보다 작은 타입은 int타입으로 자동 변환되고 연산결과 역시 int타입이 된다. 그러나 쉬프트 연산자는 다른 이항연산자들과 달리 피연산자의 타입을 일치시킬 필요가 없기 때문에 우측 피연산자에는 산술변환이 적용되지 않는다.

변수 앞에 키워드 ‘final’을 붙이면 상수가 된다. 상수는 반드시 선언과 동시에 값을 저장해야하며, 한 번 저장된 값은 바꿀 수 없다.

조건문과 반복문

JDK 1.5부터 배열과 컬렉션에 저장된 요소에 접근할 때 기존보다 편리한 방법으로 처리할 수 있도록 for문의 새로운 문법이 추가되었다.

1
2
3
for (타입 변수명 : 배열 또는 컬렉션) {
// 반복할 문장
}

위의 문장에서 타입은 배열 또는 컬렉션의 요소의 타입이어야 한다. 배열 또는 컬렉션에 저장된 값이 매 반복마다 하나씩 순서대로 읽혀서 변수에 저장된다. 그러나 향상된 for문은 일반적인 for문과 달리 배열이나 컬렉션에 저장된 요소들을 읽어오는 용도로만 사용 수 있다는 제약이 있다.

반복문은 그저 같은 문장을 반복해서 수행하는 것이지만, 메서드를 호출하는 것은 반복문 보다 몇 가지 과정, 예를 들면 매개변수 복사와 종료 후 복귀할 주소저장 등, 이 추가로 필요하기 때문에 반복문보다 재귀호출의 수행시간이 더 오래 걸린다.

배열

배열은 같은 타입의 여러 변수를 하나의 묶음으로 다루는 것이다.
배열을 선언하는 것은 단지 생성된 배열을 다루기 위한 참조변수를 위한 공간이 만들어질 뿐이고, 배열을 생성해야만 비로소 값을 저장할 수 있는 공간이 만들어지는 것이다.

Java에서는 배열의 길이가 0일 수도 있다.

배열이름.length - 자바에서는 JVM이 모든 배열의 길이를 별도로 관리하며, ‘배열이름.length’를 통해서 배열의 길이에 대한 정보를 얻을 수 있다. ‘배열이름.length’는 상수이다.

자바에서는 다음과 같이 배열을 간단히 초기화 할 수 있는 방법을 제공한다.

1
2
int[] score1 = new int[]{50, 60, 70, 80, 90}; // 배열의 생성과 초기화를 동시에
int[] score2 = {50, 60, 70, 80, 90} // new int[]를 생략할 수 있음

그러나 배열의 선언과 생성을 따로 하는 경우에는 생략할 수 없다.

1
2
3
int [] score;
score = new int[]{50, 60, 70, 80, 90}; // OK
score = {50, 60, 70, 80, 90}; // 에러. new int[]를 생략할 수 없음

만약 score의 값을 바로 출력하면 어떻게 될까? 타입@주소의 형식으로 출력된다. ‘[I’는 1차원 int 배열이라는 의미이고, ‘@’뒤에 나오는 16진수는 배열의 주소인데 실제 주소가 아닌 내부 주소이다.

1
2
// 배열을 가리키는 참조변수 score의 값을 출력
System.out.println(score); // [I@14318bb와 같은 형식의 문자열이 출력된다.

예외적으로 char배열은 println메서드로 출력하면 각 요소가 구분자없이 그대로 출력되는데, 이것은 println메서드가 char배열일 때만 이렇게 동작하도록 작성되었기 때문이다.

for문 대신 System클래스의 arraycopy()를 사용하면 보다 간단하고 빠르게 배열을 복사할 수 있다.

자바에서 char배열이 아닌 String클래스를 이용해서 문자열을 처리하는 이유는 String클래스가 char배열에 여러 가지 기능을 추가하여 확장한 것이기 때문이다.
char배열과 String클래스의 한 가지 중요한 차이는, String객체(문자열)는 읽을 수만 있을 뿐 내용을 변경할 수 없다. (변경 가능한 문자열을 다루려면, StringBuffer 클래스를 사용하면 된다.)

JVM의 메모리구조

JVM은 시스템으로부터 프로그램을 수행하는데 필요한 메모리를 할당받고 JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.

  • 메서드 영역 (method area)
    프로그램 실행 중 어떤 클래스가 사용되면, JVM은 해당 클래스의 클래스파일(*.class)을 읽어서 분석하여 클래스에 대한 정보(클래스 데이터)를 이곳에 저장한다. 그 클래스의 클래스변수도 이 영역에 함께 생성된다.
  • 힙 (heap)
    인스턴스가 생성되는 공간. 프로그램 실행 중 생성되는 인스턴스는 모두 이곳에 생성된다. 즉, 인스턴스 변수들이 생성되는 곳이다.
  • 호출스택 (call stack or execution stack)
    호출스택은 메서드의 작업에 필요한 메모리 공간을 제공한다. 메서드가 호출되면, 호출스택에 호출된 메서드를 위한 메모리가 할당되고, 이 메모리는 메서드가 작업을 수행하는 동안 지역변수(매개변수 포함)들과 연산의 중간결과 등을 저장하는데 사용된다. 메서드가 작업을 마치면 할당되었던 메모리공간은 반환되어 비워진다.

클래스

인스턴스 메서드는 인스턴스 변수와 관련된 작업을 하는, 즉 메서드의 작업을 수행하는데 인스턴스 변수를 필요로 하는 메서드이다. 반면에 인스턴스와 관계없는(인스턴스 변수나 인스턴스 메서드를 사용하지 않는) 메서드를 클래스 메서드(static 메서드)로 정의한다.

가변인자

기존에는 메서드의 매개변수 개수가 고정적이었으나 JDK1.5부터 동적으로 지정해 줄 수 있게 되었으며, 이 기능을 가변인자라고 한다. 가변인자는 **’타입… 변수명’**과 같은 형식으로 선언한다. PrintStream클래스의 printf()가 대표적인 예이다.

1
public PrintStream printf(String format, Object... args); { ... }

가변인자 외에도 매개변수가 더 있으면, 가변인자를 매개변수 중에서 제일 마지막에 선언해야 한다. 그렇지 않으면, 컴파일 에러가 발생한다.

가변인자는 내부적으로 배열을 이용한다. 그래서 가변인자가 선언된 메서드를 호출할 때마다 배열이 새로 생성된다. 가변인자가 편리하지만, 이런 비효율이 숨어있기 때문에 꼭 필요한 경우에만 사용해야한다.

가변인자를 사용한 메서드를 호출할 때는 인자가 아예 없어도 되고 배열을 사용할 수도 있다. (C언어와 달리 자바에서는 길이가 0인 배열을 생성하는 것이 허용된다.)

생성자

컴파일러가 자동적으로 기본 생성자를 추가해주는 경우는 ‘클래스 내에 생성자가 하나도 없을 때’ 뿐이다.

생성자에서 다른 생성자를 호출할 때에는 생성자의 이름으로 클래스이름 대신 this를 사용한다. ‘this’는 참조변수로 인스턴스 자신을 가리킨다. 또한 한 생성자에서 다른 생성자를 호출할 때는 반드시 첫 줄에서만 호출이 가능하다.

  • this : 인스턴스 자신을 가리키는 참조변수, 인스턴스의 주소가 저장되어 있다. 모든 인스턴스 메서드에 지역변수에 숨겨진 채로 존재한다.
  • this(), this(매개변수) : 생성자 같은 클래스의 다른 생성자를 호출할 때 사용한다.

변수의 초기화

멤버변수는 초기화를 하지 않아도 자동적으로 변수의 자료형에 맞는 기본갑으로 초기화가 이루어지므로 초기화하지 않고 사용해도 되지만, 지역변수는 사용하기 전에 반드시 초기화해야 한다. (멤버변수(클래스변수와 인스턴스변수)와 배열의 초기화는 선택적이지만, 지역변수의 초기화는 필수적이다.)

멤버변수의 초기화는 지역변수와 달리 여러 가지 방법이 있다.

  • 명시적 초기화 : 변수를 선언과 동시에 초기화하는 것
  • 생성자
  • 초기화 블럭
    • 인스턴스 초기화 블럭 : 인스턴스변수를 초기화 하는데 사용
    • 클래스 초기화 블럭 : 클래스 변수를 초기화 하는데 사용
1
2
3
4
5
6
7
class testClass() {
static { /* 클래스 초기화 블럭 */ }

{ /* 인스턴스 초기화 블럭 */ }

// ...
}

클래스 초기화 블럭은 클래스가 메모리에 처음 로딩될 때 한번만 수행되며, 인스턴스 초기화 블럭은 생성자와 같이 인스턴스를 생성할 때 마다 수행된다. (생성자보다 인스턴스 초기화 블럭이 먼저 수행된다.)

인스턴스 변수의 초기화는 주로 생성자를 사용하고, 인스턴스 초기화 블럭은 모든 생성자에서 공통으로 수행돼야 하는 코드를 넣는데 사용한다.

  • 클래스변수의 초기화 시점 : 클래스가 처음 로딩될 때 단 한번 초기화 된다.
  • 인스턴스변수의 초기화 시점 : 인스턴스가 생성될 때마다 각 인스턴스별로 초기화가 이루어진다.
  • 클래스변수의 초기화 순서 : 기본값 -> 명시적초기화 -> 클래스 초기화 블럭
  • 인스턴스변수의 초기화 순서 : 기본값 -> 명시적초기화 -> 인스턴스 초기화 블럭 -> 생성자

프로그램 실행도중 클래스에 대한 정보가 요구될 때, 클래스에 로딩된다. 하지만, 해당 클래스가 이미 메모리에 로딩되어 있다면, 또 다시 로딩하지 않는다.

클래스의 로딩 시기는 JVM의 종류에 따라 좀 다를 수 있는데, 클래스가 필요할 때 바로 메모리에 로딩하도록 설계가 되어있는 것도 있고, 실행효율을 높이기 위해서 사용될 클래스들을 프로그램이 시작될 때 미리 로딩하도록 되어있는 것도 있다.

상속

생성자와 초기화 블럭은 상속되지 않는다. 멤버만 상속된다.

오버라이딩의 조건

오버라이딩시 접근 제어자와 예외는 제한된 조건 하에서만 다르게 변경할 수 있다.

  • 접근 제어자는 조상 클래스의 메서드보다 좁은 범위로 변경 할 수 없다.
    접근범위는 public, protected, (default), private이다.
  • 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.

Object 클래스를 제외한 모든 클래스의 생성자는 첫 줄에 반드시 자신의 다른 생성자 또는 조상의 생성자를 호출해야 한다. 그렇지 않으면 컴파일러는 생성자의 첫 줄에 ‘super();’를 자동적으로 추가한다.

어떤 클래스의 인스턴스를 생성하면, 클래스 상속관계의 최고조상인 Object 클래스까지 거슬러 올라가면서 모든 조상클래스의 생성자가 순서대로 호출된다.

package와 import

패키지(package)

패키지란, 클래스의 묶음이다. 클래스의 실제 이름은 패키지명을 포함한 것이다.

클래스가 물리적으로 하나의 클래스파일(.class)인 것과 같이 패키지는 물리적으로 하나의 디렉토리이다. 디렉토리가 하위 디렉토리를 가질 수 있는 것처럼, 패키지도 다른 패키지를 포함할 수 있으며 점’.’으로 구분한다.

패키지 선언문은 반드시 소스파일에서 주석과 공백을 제외한 첫 번째 문장이어야 하며, 하나의 소스파일에 단 한번만 선언될 수 있다.

소스파일에 자신이 속할 패키지를 지정하지 않은 클래스는 자동적으로 ‘이름 없는 패키지’에 속하게 된다. 결국 패키지를 지정하지 않는 모든 클래스들은 같은 패키지에 속한다.

클래스패스(classpath)는 컴파일러(javac.exe)나 JVM 등이 클래스의 위치를 찾는데 사용되는 경로이다.

import문

클래스의 코드를 작성하기 전에 import문으로 사용하고자 하는 클래스의 패키지를 미리 명시해주면 소스코드에 사용되는 클래스이름에서 패키지명은 생략할 수 있다.

import문의 역할은 컴파일러에게 소스파일에 사용된 클래스의 패키지에 대한 정보를 제공하는 것이다.

import문에서 클래스의 이름 대신 ‘*’을 사용하는 것이 하위 패키지의 클래스까지 포함하는 것은 아니라는 것이다.

import문으로 패키지를 지정하지 않으면 위와 같이 모든 클래스이름 앞에 패키지명을 반드시 붙여야 한다. 단, 같은 패키지 내의 클래스들은 import문을 지정하지 않고도 패키지명을 생략할 수 있다.

제어자

생성자가 private인 클래스는 다른 클래스의 조상이 될 수 없다. 그래서 클래스 앞에 final을 더 추가하여 상속할 수 없는 클래스라는 것을 알려야 한다.

제어자를 조합해서 사용할 때 주의해야 할 사항

  • 메서드에 static과 abstract를 함께 사용할 수 없다. (static 메서드는 몸통이 있는 메서드에만 사용할 수 있기 때문이다.)
  • 클래스에 abstract와 final을 동시에 사용할 수 없다.

참조변수

형변환

컴파일 시에는 참조변수간의 타입만 체크하기 때문에 실행 시 생성될 인스턴스의 타입에 대해서는 전혀 알지 못한다. 그래서 컴파일 시에는 문제가 없었지만, 실행 시에는 에러가 발생하여 실행이 비정상적으로 종료될 수 있다.

참조변수가 참좋고 있는 인스턴스의 실제 타입을 알아보기 위해 instance 연산자를 사용한다. 실제 인스턴스와 같은 타입의 instanceof 연산 이외에 조상타입의 instance 연산에도 true를 결과로 얻으며, instanceof의 연산의 결과가 true라는 것은 검사한 타입으로의 형변환을 해도 아무런 문제가 없다는 뜻이다.

참조변수와 인스턴스의 연결

멤버변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우, 조상 타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 멤버변수가 사용되고, 자손타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버변수가 사용된다. 하지만 중복 정의되지 않은 경우, 조상타입의 참조변수를 사용했을 때와 자손타입의 참조변수를 사용했을 때의 차이는 없다.

인터페이스

인터페이스는 일종의 추상클래스이다. 인터페이스는 추상클래스처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다. 오직 추상메서드와 상수만을 멤버로 가질 수 있다.

  • 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.
  • 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.
    단, static 메서드와 디폴트 메서드는 예외

원래는 인터페이스의 모든 메서드는 추상메서드이어야 하는데, JDK1.8부터 인터페이스에 static 메서드와 디폴트 메서드의 추가를 허용하는 방향으로 변경되었다.

디폴트 메서드

조상 클래스에 새로운 메서드를 추가하는 것은 별 일이 아니지만, 인터페이스의 경우에는 보통 큰 일이 아니다. 인터페이스에 메서드를 추가한다는 것은, 추상 메서드를 추가한다는 것이고, 이 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해야 하기 때문이다.

디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다.

디폴트 메서드는 메서드 앞에 키워드 default를 붙이며, 추상 메서드와 달리 일반 메서드처럼 몸통{}이 있어야 한다. 디폴트 메서드 역시 접근 제어자가 public 이며, 생략가능하다.

내부 클래스

내부 클래스는 클래스 내에 선언된 클래스이다. 클래스에 다른 클래스를 선언하는 이유는 간단하다. 두 클래스가 서로 긴밀한 관계에 있기 때문이다.

내부 클래스의 장점

  • 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.
  • 코드의 복잡성을 줄일 수 있다(캡슐화).

내부 클래스의 종류와 특징

내부 클래스의 종류는 변수의 선언위치에 따른 종류와 같다. 내부 클래스는 마치 변수를 선언하는 것과 같은 위치에 선언할 수 있으며, 변수의 선언위치에 따라 인스턴스변수, 클래스변수(static 변수), 지역변수로 구분되는 것과 같이 내부 클래스도 선언위치에 따라 다음과 같이 구분된다.

  • 인스턴스 클래스 (instance class)
    외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 인스턴스 멤버처럼 다루어 진다. 주로 외부 클래스의 인스턴스멤버들과 관련된 작업에 사용될 목적으로 선언된다.
  • 스태틱 클래스 (static class)
    외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 static 멤버처럼 다루어진다. 주로 외부 클래스의 static 멤버, 특히 static 메서드에서 사용될 목적으로 선언된다.
  • 지역 클래스 (local class)
    외부 클래스의 메서드나 초기화블럭 안에 선언하며, 선언된 영역 내부에서만 사용될 수 있다.
  • 익명 클래스 (anonymous class)
    클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스(일회용)

지역 클래스(LocalInner)는 외부 클래스의 인스턴스멤버와 static 멤버를 모두 사용할 수 있으며, 지역 클래스가 포함된 메서드에 정의된 지역변수도 사용할 수 있다. 단, final이 붙은 지역변수만 접근가능한데 그 이유는 메서드가 수행을 마쳐서 지역변수가 소멸된 시점에도, 지역 클래스의 인스턴스가 소멸된 지역변수를 참조하려는 경우가 발생할 수 있기 때문이다.

JDK 1.8부터 지역 클래스에서 접근하는 지역 변수 앞에 final을 생략할 수 있게 바뀌었다. 대신 컴파일러가 자동으로 붙여준다. 즉, 편의상 final을 생략할 수 있게 한 것일 뿐 해당 변수의 값이 바뀌는 문장이 있으면 컴파일 에러가 발생한다.

내부 클래스는 컴파일 했을 때 생성되는 파일명은 외부 클래스명$내부 클래스명.class 형식으로 되어 있다.

익명 클래스 (anonymous class)

익명클래스는 특이하게도 다른 내부 클래스들과는 달리 이름이 없다. 클래스의 선언과 객체의 생성을 동시에 하기 때문에 단 한번만 사용될 수 있고 오직 하나의 객체만을 생성할 수 있는 일회용 클래스이다.

1
2
3
4
5
6
7
new 조상클래스이름() {
// 멤버 선언
}
또는
new 구현인터페이스이름() {
// 멤버 선언
}

이름이 없기 때문에 생성자도 가질 수 없으며, 조상클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해서 정의하기 때문에 하나의 클래스로 상속받는 동시에 인터페이스를 구현하거나 둘 이상의 인터페이스를 구현할 수 있다. 오로지 단 하나의 클래스를 상속받거나 단 하나의 인터페이스만을 구현할 수 있다.

익명 클래스는 이름이 없기 때문에 외부 클래스명$숫자.class의 형식으로 클래스파일명이 결정된다.

예외처리

프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우가 있다. 이러한 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 한다.

이를 발생시점에 따라 컴파일 에러런타임 에러로 나눌 수 있다.

컴파일 에러 - 컴파일 시에 발생하는 에러

런타임 에러 - 실행 시에 발생하는 에러

논리적 에러 - 실행은 되지만, 의도와 다르게 동작하는 것

컴파일은 잘되었어도 실행 중에 에러에 의해서 잘못된 결과를 얻거나 프로그램이 비정상적으로 종료될 수 있다.

자바에서는 실행 시(runtime) 발생할 수 있는 프로그램 오류를 에러(error)예외(exception) 두 가지로 구분하였다.

에러는 메모리 부족(OutOfMemoryError)이나 스택오버플로우(StackOverflowError)와 같이 일단 발생하면 복구할 수 없는 심각한 오류이고, 예외는 발생하더라도 수습될 수 있는 비교적 덜 심각한 것이다.

모든 예외의 최고 조상은 Exception 클래스이다.

예외 처리하기 - try-catch 문

예외처리의

정의 - 프로그램 실행 시 발생할 수 있는 예외의 발생에 대비한 코드를 작성하는 것

목적 - 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것

에러와 예외는 모두 실행 시(runtime) 발생하는 오류이다.

발생한 예외를 처리하지 못하면, 프로그램은 비정상적으로 종료되며, 처리되지 못한 예외(uncaught exception)는 JVM의 ‘예외처리기(UncaughtExceptionHandler)’가 받아서 예외의 원인을 화면에 출력한다.

예외를 처리하기 위해서는 try-catch문을 사용한다. 하나의 try 블럭 다음에는 여러 종류의 예외를 처리할 수 있도록 하나 이상의 catch 블럭이 올 수 있으며, 이 중 발생한 예외의 종류와 일치하는 단 한 개의 catch 블럭만 수행된다.

try 블럭 또는 catch 블럭에 또 다른 try-catch 문이 포함될 수 있다. catch 블럭 내의 코드에서도 예외가 발생할 수 있기 때문이다. catch 블럭 내에 또 하나의 try-catch 문이 포함된 경우, 같은 이름의 참조변수를 사용해서는 안 된다.

정수는 0으로 나누는 것이 금지되어 있지만, 실수를 0으로 나누는 것은 금지되어있지 않으며 예외가 발생하지 않는다.

try 블럭에서 예외가 발생하면, 예외가 발생한 위치 이후에 있는 try 블럭의 문장들은 수행되지 않으므로, try 블럭에 포함시킬 코드의 범위를 잘 선택해야 한다.

예외가 발생한 문장이 try-catch 블럭부터 차례로 내려가면서 catch 블럭의 괄호()내에 선언된 참조변수의 종류와 생성된 예외클래스의 인스턴스에 instanceof 연산자를 이용해서 검사하게 되는데, 검사 결과가 true인 catch 블럭을 만날 때까지 검사는 계속된다.

printStackTrace()와 getMessage()

예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨져 있으며, getMessage()와 printStackTrace()를 통해서 이 정보들을 얻을 수 있다.

printStackTrace() - 예외발생 당시의 호출스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.

getMessage() - 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.

printStackTrace(PrintStream s) 또는 printStackTrace(PrintWriter s)를 사용하면 발생한 예외에 대한 정보를 파일에 저장할 수도 있다.

멀티 catch 블럭

JDK 1.7부터 여러 catch 블럭을 ‘|’ 기호를 이용해서, 하나의 catch 블럭으로 합칠 수 있게되었으며, 이를 ‘멀티 catch 블럭’이라 한다. (멀티 catch 블럭에 사용되는 ‘|’는 논리 연산자가 아니라 기호이다.)

만일 멀티 catch 블럭의 ‘|’ 기호로 연결된 예외 클래스가 조상과 자손의 관계에 있다면 컴파일 에러가 발생한다. (그냥 조상 클래스만 써주는 것과 똑같기 때문이다.)

예외 발생시키기

키워드 throw를 사용해서 프로그래머가 고의로 예외를 발생시킬 수 있다.

  1. 먼저, 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든 다음
  2. 키워드 throw를 이용해서 예외를 발생시킨다.

Exception 인스턴스를 생성할 때, 생성자에 String을 넣어 주면, 이 String이 Exception 인스턴스에 메시지로 저장된다. 이 메시지는 getMessage()를 이용해서 얻을 수 있다.

RuntimeException을 발생시키는 코드는 이에 대한 예외 처리를 하지 않았음에도 불구하고 성공적으로 컴파일 된다. RuntimeException 클래스들과 그 자손 클래스에 해당하는 예외는 프로그래머가 실수로 발생하는 것들이기 때문에 예외처리를 강제하지 않는 것이다.

컴파일러가 예외처리를 확인하지 않는 RuntimeException 클래스들은 unchecked 예외라고 부르고, 예외처리를 확인하는 Exception 클래스들은 checked 예외라고 부른다.

Error와 그 자손도 unchecked 예외이다. try-catch 블럭으로 처리할 수 없기 때문이다.

메서드에 예외 선언하기

예외를 처리하는 방법에는 try-catch 문을 사용하는 것 외에, 예외를 메서드에 선언하는 방법이 있다. 메서드에 예외를 선언하려면, 메서드의 선언부에 키워드 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 된다. (예외를 발생시키는 키워드 throw와 예외를 메서드에 선언할 때 쓰이는 throws를 구별해야 한다.)

메서드에 예외를 선언할 때 일반적으로 RuntimeException 클래스들은 적지 않는다.

예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아니라, 자신(예외가 발생할 가능성이 있는 메서드)을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다.

예외를 전달받은 메서드가 또다시 자신을 호출한 메서드에게 전달할 수 있으며, 이런 식으로 계속 호출스택에 있는 메서드들을 따라 전달되다가 제일 마지막에 있는 main 메서드에서도 예외가 처리되지 않으면, main 메서드 마저 종료되어 프로그램이 전체가 종료된다.

finally 블럭

finally 블럭은 try-catch 문과 함께 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용된다.

try 블럭에서 return문이 실행되는 경우에도 finally 블럭의 문장들이 먼저 실행된 후에, 현재 실행 중인 메서드를 종료한다. 마찬가지로 catch 블럭의 문장 수행 중에 return 문을 만나도 finally 블럭의 문장들은 수행된다.

자원 자동 반환 - try-with-resources문

JDK 1.7부터 try-with-resources문이라는 try-catch문의 변형이 새로 추가되었다.

try-with-resources문의 괄호()안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close()를 호출하지 않아도 try 블럭을 벗어나는 순간 자동적으로 close()가 호출된다. 그 다음에 catch 블럭 또는 finally 블럭이 수행된다.

try-with-resources문에 의해 자동으로 객체의 close()가 호출될 수 있으려면, 클래스가 AuthCloseable이라는 인터페이스를 구현한 것이어야만 한다.

참고

자주 보지 않으면 잊게되는 Java 기초 정리

https://jongmin92.github.io/2018/04/07/Java/java-basic/

Author

KimJongMin

Posted on

2018-04-07

Updated on

2021-03-22

Licensed under

댓글