본문 바로가기

UIUX Full-Stack Developer

[0629] 자바에 대한 모든 것

# 내용에 들어가기 전에 "점프투자바"를 참고하였다.

# 점프투자바에서 내가 공부하고 싶은 부분만 추리고, 이해한 부분만 인용하였다. (혼공)

# 즉 내가 아는건 안 썼음~

# 2시간 동안 공부하면서 점프투자바 마스터한다 ! ! !

\자바는 썬 마이크로시스템즈의 제임스 고슬링(James Gosling)과 다른 연구원들이 개발한 객체 지향적 프로그래밍 언어로 1995년에 발표되었다. 처음에는 가전제품 내에 탑재해 동작하는 프로그램을 위해 개발했지만, 현재는 웹 애플리케이션과 모바일 앱 개발에 가장 많이 사용하는 언어로 성장했다.

자바는 특히 우리나라에서 차지하는 비중이 상당하다. 우리나라 기업에서 사용하는 프로그램의 80% 이상은 자바로 만들어졌다고 해도 과언이 아니다. 또한 자바는 우리나라에서 오랜 시간 굳건한 생태계를 구축하고 유지했기 때문에 자바로 만들어진 수많은 라이브러리가 존재한다. 따라서 우리나라 기업에서 개발자로 일하고 싶다면 자바 언어를 배울 것을 강력히 추천한다.

보통 일반적인 자바 소스코드는 다음과 같은 형태로 만들어 진다.

클래스명.java

/* 클래스 블록 */
public class 클래스명 {

    /* 메서드 블록 */
    [public|private|protected] [static] (리턴자료형|void) 메서드명1(입력자료형 매개변수, ...) {
        명령문(statement);
        ...
    }

    /* 메서드 블록 */
    [public|private|protected] [static] (리턴자료형|void) 메서드명2(입력자료형 매개변수, ...) {
        명령문(statement);
        ...
    }

    ...
}

소스코드의 가장 바깥쪽 영역은 클래스 블록이다. 클래스명은 원하는 이름으로 지을 수 있다. 단, 클래스명은 소스파일의 이름(클래스명.java)과 동일하게 사용해야 한다.

참고: public 클래스

그리고 클래스 블록은 메서드 블록들을 포함한다.

이제 메서드블록을 살펴보자. 메서드 블록 부분에서 [public|private|protected] 라고 되어 있는 부분의 의미는 public, private, protected 또는 아무것도 오지 않을 수 있다는 의미이다. public, private, protected는 메서드의 접근제어자를 뜻하는데 이것에 대해서는 뒤에서 자세히 다루고 있다.

참고: 접근제어자

그 다음의 [static] 은 static 키워드가 올수도 있고 오지 않을 수도 있다는 의미이다. static 이라는 키워드가 붙게 되면 static 메서드가 되는데 이것에 대한 내용도 뒤에서 자세히 다루고 있다.

참고: 정적메서드와 변수

그 다음의 (리턴자료형|void) 는 메서드가 실행된 후 리턴되는 값의 자료형을 의미한다. 리턴값이 있을 경우에는 반드시 리턴 자료형을 표기해야 하며 만약 리턴값이 없는 경우라면 void 로 표기해야 한다. 이 항목은 둘 다 생략할 수는 없고 void 또는 리턴자료형이 반드시 있어야만 한다. (그래서 [] 대신 () 표기로 표시하였다.)

메서드명은 원하는 이름으로 지을수 있다. 메서드 명 이후의 괄호() 안의 값들은 메서드의 입력 인자를 뜻한다. 입력 인자의 갯수는 제한이 없으며 입력 인자는 "입력자료형"+"매개변수명" 형태로 이루어 진다.

클래스내에는 이러한 메서드를 여러개 만들수 있다.

 

속성과 생성자

자바 소스코드에는 속성과 생성자도 구성요소로 포함된다. 속성과 생성자는 5장 객체지향 프로그래밍에서 자세히 다루므로 여기서는 눈으로만 간단히 알아보자.

public class Sample {

    /* 속성 */
    private String message;

    /* 생성자 */
    public Sample(String message) {
        this.message = message
    }
}

속성은 클래스의 상태를 나타내는 변수로 클래스 내부에 선언된다. 생성자는 클래스 인스턴스를 생성할 때 실행되는 메서드로 주로 속성을 초기화 하는 용도로 사용한다.

소스코드의 예

다음의 가장 간단한 샘플로 실제 소스코드의 구조에 대해서 자세히 알아보자.

Sample.java

public class Sample {
    public static void main(String[] args) {
        System.out.println("Hello java");
    }
}

클래스 블록

소스코드의 가장 바깥쪽 영역인 클래스 블록을 보자.

public class Sample {
    (... 생략 ...)
}

이 클래스의 클래스명은 Sample이다.

소스코드의 가장 바깥쪽은 클래스(class) 블록이다. 그리고 클래스 블록은 중괄호({})로 둘러싸야 한다.

  • { - 블록의 시작
  • } - 블록의 끝

public은 자바의 접근제어자로 어디서든 이 클래스에 접근할 수 있음을 의미한다. class는 클래스 블록을 만드는 키워드이다.

접근 제어자에 대해서는 뒤에서 자세하게 다룬다.

메서드 블록

클래스 블록 안에는 다음과 같은 메서드 블록이 있다.

public class Sample {
    public static void main(String[] args) {
        (... 생략 ...)
    }
}

이 메서드의 메서드명은 main이다.

메서드 블록 역시 중괄호로 영역을 구분한다. 클래스 블록 안에는 여러개의 메서드 블록이 있을 수 있다. 메서드 블록에 사용된 public, static, void 등에 대해서는 뒤에서 자세히 다루지만 간단히 알아보자.

  • static - 메서드에 static 키워드가 붙을 경우 이 메서드는 클래스 메서드가 되어 객체를 만들지 않아도 "클래스명.메서드명" 형태로 호출이 가능하다.
  • void - 메서드의 리턴타입 중 하나로 void는 리턴값이 없음을 의미한다.
  • String[] args - 메서드의 매개 변수이다. args 변수는 String[] 배열 자료형임을 의미한다. args라는 이름은 인수를 의미하는 arguments의 약어로 관례적인 이름이다. args 대신 다른 이름을 사용해도 상관없다.

명령문 (Statement)

마지막으로 메서드 블록 안에는 명령문(Statement)이 있다.

public class Sample {
    public static void main(String[] args) {
        System.out.println("Hello java");
    }
}

위와 같이 컴퓨터에 무언가 일을 시키는 문장을 명령문(Statement)이라고 한다. 명령문은 반드시 세미콜론(;)을 붙여 문장의 끝을 표시해야 한다. 메서드 블록 안에는 여러개의 명령문이 있을 수 있다.

 

리터럴 표기

String a = "happy java"  String b = new String("happy java")에서 a, b 변수는 동일한 문자열 값을 갖게 되지만 완전히 동일하지는 않다. 첫번째 방식은 리터럴(literal) 표기라고 하는데 객체 생성없이 고정된 값을 그대로 대입하는 방법을 말한다. 좀 더 자세히 알아보면, 리터럴 표기법은 "happy java" 라는 문자열을 JVM의 intern pool 이라는 메모리 공간에 저장하고 다음에 다시 동일한 문자열이 선언될때는 cache 된 문자열을 리턴한다. 이와는 달리 두번째 방식은 항상 새로운 String 객체를 만든다.

 

equals

equals는 두개의 문자열이 동일한지를 비교하여 결과값을 리턴한다.

다음의 예를 보자.

String a = "hello";
String b = "java";
String c = "hello";
System.out.println(a.equals(b)); // false 출력
System.out.println(a.equals(c)); // true 출력
false
true

문자열 a 와 문자열 b 는 "hello"와 "java"로 서로 같지 않다. 따라서 equals 메서드 호출 시 false 를 리턴한다. 문자열 a 와 문자열 c 는 "hello"와 "hello"로 서로 같다. 따라서 true 를 리턴한다.

문자열의 값을 비교할때는 반드시 equals 를 사용해야 한다. == 연산자를 사용할 경우 다음과 같은 경우가 발생할 수 있다.

String a = "hello";
String b = new String("hello");
System.out.println(a.equals(b));  // true
System.out.println(a == b);  // false
true
false

문자열 a와 b는 모두 "hello"로 같은 값이지만 equals 를 호출했을 때는 true 를, == 연산자를 이용했을 때는 false를 리턴한다. a와 b는 값은 같지만 서로 다른 객체이다. == 은 두개의 자료형이 동일한 객체인지를 판별할 때 사용하는 연산자이기 때문에 false를 리턴한다.

객체에 대해서는 "객체 지향 프로그래밍" 챕터에서 자세하게 다룬다.

indexOf

indexOf는 문자열에서 특정 문자열이 시작되는 위치(인덱스)를 리턴한다.

문자열 중 "Java"라는 문자열이 시작되는 위치를 알고 싶은 경우 indexOf를 다음과 같이 사용할 수 있다.

String a = "Hello Java";
System.out.println(a.indexOf("Java"));  // 6 출력
6

"Hello Java" 라는 문자열에서 "Java"라는 문자열은 일곱번째 문자('J')부터 시작이다. 결과값이 7이 아닌 6으로 나온 이유는 자바는 숫자를 0부터 세기 때문이다.

contains

contains는 문자열에서 특정 문자열이 포함되어 있는지의 여부를 리턴한다.

다음의 예를보자.

String a = "Hello Java";
System.out.println(a.contains("Java"));  // true 출력
true

문자열 a는 "Java"라는 문자열을 포함하고 있기 때문에 true를 리턴한다.

charAt

charAt은 문자열에서 특정 위치의 문자(char)를 리턴한다.

"Hello Java" 문자열에서 "J"라는 문자는 6번째 인덱스에 위치한 문자이다. 6이라는 숫자로 "J"라는 문자를 얻기 위해서는 다음과 같이 charAt을 사용한다.

String a = "Hello Java";
System.out.println(a.charAt(6));  // "J" 출력
J

replaceAll

replaceAll은 문자열 중 특정 문자열을 다른 문자열로 바꾸고자 할 때 사용한다.

String a = "Hello Java";
System.out.println(a.replaceAll("Java", "World"));  // Hello World 출력
Hello World

"Hello Java" 문자열에서 "Java"를 "World"로 바꾸었다.

substring

substring은 문자열 중 특정 부분을 뽑아낼 경우에 사용한다.

다음의 예를 보자.

String a = "Hello Java";
System.out.println(a.substring(0, 4));  // Hell 출력
Hell

위처럼 substring(시작위치, 끝위치)와 같이 사용하면 문자열의 시작위치에서 끝위치까지의 문자를 뽑아내게 된다. 단 끝위치는 포함이 안된다는 점에 주의하자. 이것은 다음과 같은 수학의 식과 비슷하다.

시작위치 <= a < 끝위치

toUpperCase

toUpperCase는 문자열을 모두 대문자로 변경할 때 사용한다. (모두 소문자로 변경할때는 toLowerCase를 사용한다.)

String a = "Hello Java";
System.out.println(a.toUpperCase());  // HELLO JAVA 출력
HELLO JAVA

split

split 메서드는 문자열을 특정 구분자로 나누어 문자열 배열로 리턴하는 메서드이다.

String a = "a:b:c:d";
String[] result = a.split(":");  // result는 {"a", "b", "c", "d"}

위의 예처럼 "a:b:c:d" 라는 문자열을 ":" 문자로 나누어 {"a", "b", "c", "d"} 문자열 배열을 만들수 있다.

배열에 대해서는 조금후에 자세히 알아보자.

문자열 포매팅

문자열에서 또 하나 알아야 할 것으로는 문자열 포매팅(Formatting)이 있다. 이것을 공부하기 전에 다음과 같은 문자열을 출력하는 프로그램을 작성했다고 가정해 보자.

"현재 온도는 18도입니다."

시간이 지나서 20도가 되면 다음 문장을 출력한다.

"현재 온도는 20도입니다"

위 두 문자열은 모두 같은데 20이라는 숫자와 18이라는 숫자만 다르다. 이렇게 문자열 안의 특정한 값을 바꿔야 할 경우가 있을 때 이것을 가능하게 해주는 것이 바로 문자열 포매팅 기법이다.

쉽게 말해 문자열 포매팅이란 문자열 안에 어떤 값을 삽입하는 방법이다. 다음 예를 직접 실행해 보면서 그 사용법을 알아보자.

문자열 포매팅 따라 하기

1. 숫자 바로 대입

System.out.println(String.format("I eat %d apples.", 3));  // "I eat 3 apples." 출력
I eat 3 apples.

문자열 포매팅은 String.format 메서드를 사용한다. 위 예제의 결괏값을 보면 알겠지만 위 예제는 문자열 안에 정수 3을 삽입하는 방법을 보여 준다. 문자열 안에서 숫자를 넣고 싶은 자리에 %d 문자를 넣어 주고, 삽입할 숫자 3은 두번째 파라미터로 전달했다. 여기에서 %d는 문자열 포맷 코드라고 부른다.

2. 문자열 바로 대입

문자열 안에 꼭 숫자만 넣으라는 법은 없다. 이번에는 숫자 대신 문자열을 넣어 보자.

System.out.println(String.format("I eat %s apples.", "five"));  // "I eat five apples." 출력
I eat five apples.

위 예제에서는 문자열 안에 또 다른 문자열을 삽입하기 위해 앞에서 사용한 문자열 포맷 코드 %d가 아닌 %s를 썼다. 어쩌면 눈치 빠른 독자는 벌써 유추하였을 것이다. 숫자를 넣기 위해서는 %d를 써야 하고, 문자열을 넣기 위해서는 %s를 써야 한다는 사실을 말이다.

3. 숫자 값을 나타내는 변수로 대입

int number = 3;
System.out.println(String.format("I eat %d apples.", number));  // "I eat 3 apples." 출력
I eat 3 apples.

1번처럼 숫자를 바로 대입하나 위 예제처럼 숫자 값을 나타내는 변수를 대입하나 결과는 같다.

4. 2개 이상의 값 넣기

그렇다면 문자열 안에 1개가 아닌 여러 개의 값을 넣고 싶을 땐 어떻게 해야 할까?

int number = 10;
String day = "three";
System.out.println(String.format("I ate %d apples. so I was sick for %s days.", number, day));
I ate 10 apples. so I was sick for three days.

위 예문처럼 2개 이상의 값을 넣으려면 파라미터를 순서대로 전달하면 된다.

문자열 포맷 코드

문자열 포매팅 예제에서는 대입해 넣는 자료형으로 정수와 문자열을 사용했으나 이 외에도 다양한 것을 대입할 수 있다.

문자열 포맷 코드에 대해 자세히 알아보자.

코드설명

%s 문자열(String)
%c 문자 1개(character)
%d 정수(Integer)
%f 부동소수(floating-point)
%o 8진수
%x 16진수
%% Literal % (문자 % 자체)

여기에서 재미있는 것은 %s 포맷 코드인데, 이 코드는 어떤 형태의 값이든 변환해 넣을 수 있다. 무슨 말인지 예를 통해 확인해 보자.

System.out.println(String.format("I have %s apples",  3));  // "I have 3 apples" 출력
System.out.println(String.format("rate is %s", 3.234));  // "rate is 3.234" 출력
I have 3 apples
rate is 3.234

3을 문자열 안에 삽입하려면 %d를 사용하고, 3.234를 삽입하려면 %f를 사용해야 한다. 하지만 %s를 사용하면 이런 것을 생각하지 않아도 된다. 왜냐하면 %s는 자동으로 전달되는 파라미터 값을 문자열로 바꾸어 사용하기 때문이다.

점프 투 자바[포매팅 연산자 %d와 %를 같이 쓸 때는 %%를 쓴다]

System.out.println(String.format("Error is %d%.", 98));

위 예문의 결괏값으로 당연히 "Error is 98%."가 출력될 것이라고 예상하겠지만 오류(UnknownFormatConversionException)가 발생한다.

Exception in thread "main" java.util.UnknownFormatConversionException: Conversion = '.'
(... 생략 ...)

이유는 문자열 포맷 코드인 %d와 %가 같은 문자열 안에 존재하는 경우, %를 나타내려면 반드시 %%로 써야 하는 법칙이 있기 때문이다. 이 점은 꼭 기억해 두어야 한다. 하지만 문자열 안에 %d 같은 포매팅 연산자가 없으면 %는 홀로 쓰여도 상관이 없다.

따라서 위 예를 제대로 실행하려면 다음과 같이 해야 한다.

System.out.println(String.format("Error is %d%%.", 98));  // "Error is 98%." 출력
Error is 98%.

포맷 코드와 숫자 함께 사용하기

위에서 보았듯이 %d, %s 등의 포맷 코드는 문자열 안에 어떤 값을 삽입하기 위해 사용한다. 하지만 포맷 코드를 숫자와 함께 사용하면 더 유용하게 사용할 수 있다. 다음 예를 보고 따라해 보자.

1. 정렬과 공백

System.out.println(String.format("%10s", "hi"));  // "        hi" 출력
        hi

앞의 예문에서 %10s는 전체 길이가 10개인 문자열 공간에서 대입되는 값을 오른쪽으로 정렬하고 그 앞의 나머지는 공백으로 남겨 두라는 의미이다.

그렇다면 반대쪽인 왼쪽 정렬은 %-10s가 될 것이다. 확인해 보자.

System.out.println(String.format("%-10sjane.", "hi"));  // "hi        jane." 출력
hi        jane.

hi를 왼쪽으로 정렬하고 나머지는 공백으로 채웠음을 볼 수 있다.

2. 소수점 표현하기

System.out.println(String.format("%.4f", 3.42134234));  // 3.4213 출력
3.4213

3.42134234를 소수점 네 번째 자리까지만 나타내고 싶은 경우에는 위와 같이 사용한다. 즉 여기서 '.'의 의미는 소수점 포인트를 말하고 그 뒤의 숫자 4는 소수점 뒤에 나올 숫자의 개수를 말한다.

다음 예는 숫자 3.42134234를 소수점 네 번째 자리까지만 표시하고 전체 길이가 10개인 문자열 공간에서 오른쪽으로 정렬하는 예를 보여 준다.

System.out.println(String.format("%10.4f", 3.42134234));  // '    3.4213' 출력
    3.4213

System.out.printf

String.format 메서드는 포매팅된 문자열을 리턴한다. 따라서 포매팅된 문자열을 출력하려면 다음처럼 System.out.println 메서드를 함께 사용해야 한다.

System.out.println(String.format("I eat %d apples.", 3));

하지만 System.out.printf 메서드를 사용하면 String.format 메서드 없이도 동일한 형식으로 포매팅된 문자열을 출력할 수 있다.

System.out.printf("I eat %d apples.", 3);  // "I eat 3 apples." 출력
I eat 3 apples.

String.format  System.out.printf의 차이는 전자는 문자열을 리턴하는 메서드이고 후자는 문자열을 출력하는 메서드라는 점이다. 상황에 맞게 선택하여 사용하자.

 

StringBuffer는 문자열을 추가하거나 변경 할 때 주로 사용하는 자료형이다. StringBuffer의 메서드들을 살펴보면서 StringBuffer의 사용법에 대해서 알아보자.

append

다음은 StringBuffer 객체를 생성하고 문자열을 생성하는 예제이다.

StringBuffer sb = new StringBuffer();  // StringBuffer 객체 sb 생성
sb.append("hello");
sb.append(" ");
sb.append("jump to java");
String result = sb.toString();
System.out.println(result);  // "hello jump to java" 출력
hello jump to java

StringBuffer 자료형은 append 메서드를 사용하여 계속해서 문자열을 추가해 나갈 수 있다. 그리고 위 예제와 같이 toString() 메서드를 이용하여 String 자료형으로 변경할 수 있다.

위 예제를 StringBuffer 대신 String 자료형을 사용하도록 변경해 보자.

String result = "";
result += "hello";
result += " ";
result += "jump to java";
System.out.println(result);  // "hello jump to java" 출력
hello jump to java

두 개 예제의 결과는 동일하지만 내부적으로 객체가 생성되고 메모리가 사용되는 과정은 다르다.

첫번 째 예제의 경우 StringBuffer 객체는 한번만 생성된다. 두번 째 예제는 String 자료형에 + 연산이 있을 때마다 새로운 String 객체가 생성된다(문자열 간 + 연산이 있는 경우 자바는 자동으로 새로운 String 객체를 만들어 낸다). 두번 째 예제에서는 총 4개의 String 자료형 객체가 만들어진다.

String 자료형은 한번 값이 생성되면 그 값을 변경할 수가 없다. 이렇게 값을 변경할 수 없는 것을 immutable 하다고 한다. trim, toUpperCase 등의 메서드를 보면 문자열이 변경되는 것 처럼 생각 될 수도 있지만 해당 메서드 수행 시 또 다른 String 객체를 생성하여 리턴할 뿐이다. 하지만 StringBuffer는 이와 반대로 값을 변경할 수 있다(mutable 하다). 즉 한번 생성된 값을 언제든지 수정할 수 있다.

그렇다면 무조건 StringBuffer를 사용하는 것이 좋을까?

그건 상황에 따라 다르다. StringBuffer 자료형은 String 자료형보다 무거운 편에 속한다. new StringBuffer() 로 객체를 생성하는 것은 일반 String을 사용하는 것보다 메모리 사용량도 많고 속도도 느리다. 따라서 문자열 추가나 변경 등의 작업이 많을 경우에는 StringBuffer를, 문자열 변경 작업이 거의 없는 경우에는 그냥 String을 사용하는 것이 유리하다.

점프 투 자바StringBuilder

StringBuffer와 비슷한 자료형으로 StringBuilder가 있다. StringBuilder의 사용법은 StringBuffer의 사용법과 동일하다.

StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.append(" ");
sb.append("jump to java");
String result = sb.toString();
System.out.println(result);

StringBuffer는 멀티 스레드 환경에서 안전하다는 장점이 있고 StringBuilder는 StringBuffer보다 성능이 우수한 장점이 있다. 따라서 동기화를 고려할 필요가 없는 상황에서는 StringBuffer 보다는 StringBuilder를 사용하는 것이 유리하다.

insert

insert 메서드는 특정 위치에 원하는 문자열을 삽입할 수 있다.

StringBuffer sb = new StringBuffer();
sb.append("jump to java");
sb.insert(0, "hello ");
System.out.println(sb.toString());
hello jump to java

insert를 사용하여 0 번째 위치에 "hello " 라는 문자열을 삽입했다.

substring

substring 메서드는 String 자료형의 substring 메서드와 동일하게 동작한다.

StringBuffer sb = new StringBuffer();
sb.append("Hello jump to java");
System.out.println(sb.substring(0, 4));
Hell

substring(시작위치, 끝위치)와 같이 사용하면 StringBuffer 객체의 시작위치에서 끝위치까지의 문자를 뽑아낸다.

 

리스트는 배열과 비슷한 자바의 자료형으로 배열보다 편리한 기능을 많이 가지고 있다.

리스트와 배열의 가장 큰 차이는 배열은 크기가 정해져 있지만 리스트는 크기가 정해져 있지 않고 동적으로 변한다는 점이다. 예를 들어 배열의 크기를 10개로 정했다면 10개 이상의 값을 담을 수는 없다. 하지만 리스트는 크기가 정해져 있지 않아 원하는 만큼의 값을 담을 수 있다.

박찬호 선수의 투구스피드를 저장해야 한다고 가정해 보자. 배열을 이용하여 투구수를 저장할 수 있을까? 1회에 투구수가 3개도 될 수 있고 100개도 될 수 있지 않은가? 프로그래밍 시 크기를 알 수 있는 상황도 있지만 명확한 크기를 알 수 없는 경우가 더 많다. 이렇게 동적으로 자료형의 갯수가 변하는 상황이라면 List를 사용해야 할 것이다.

ArrayList

List 자료형 중 가장 간단한 형태의 자료형인 ArrayList에 대해서 알아보자.

점프 투 자바List 자료형

List 자료형에는 ArrayList, Vector, LinkedList 등의 List 인터페이스를 구현한 자료형이 있다. 여기서 말하는 List 자료형은 인터페이스인데 인터페이스에 대해서는 뒤에서 자세히 다루도록 한다.

add

박찬호 선수가 총 3번의 투구를 138, 129, 142(km) 의 속도록 던졌다면 다음과 같이 코드를 작성할 수 있다.

import java.util.ArrayList;

public class Sample {
    public static void main(String[] args) {
        ArrayList pitches = new ArrayList();
        pitches.add("138");
        pitches.add("129");
        pitches.add("142");
    }
}

ArrayList를 사용하기 위해서는 import java.util.ArrayList와 같이 ArrayList를 먼저 import해야 한다.

ArrayList 객체인 pitches에 add 라는 메서드를 이용하여 투구 스피드를 저장했다. 만약 첫번째 위치에 "133"이라는 투구 스피드를 삽입하고 싶다면 아래와 같이 삽입할 위치를 파라미터로 넘겨주어야 한다.

pitches.add(0, "133");    // 첫번째 위치에 133 삽입.

만약 2번 째 위치에 133을 삽일 할 경우에는 다음과 같이 하면 된다.

pitches.add(1, "133");

점프 투 자바제네릭스

자바는 J2SE 5.0 버전 이후부터 ArrayList<String> pitches = new ArrayList<>(); 처럼 객체를 포함하는 자료형도 어떤 객체를 포함하는지에 대해서 명확하게 표현할 것을 권고하고 있다. 인텔리제이에서 위의 예제와 같이 제네릭스 없이 코딩하면 명확한 타입을 명시하라는 warning이 표시될 것이다. 이 부분에 대한 자세한 내용은 잠시 후에 자세하게 설명한다.

get

박찬호 선수의 2번째 투구 스피드를 출력하고 싶다면 다음과 같이 한다.

import java.util.ArrayList;

public class Sample {
    public static void main(String[] args) {
        ArrayList pitches = new ArrayList();
        pitches.add("138");
        pitches.add("129");
        pitches.add("142");
        System.out.println(pitches.get(1));
    }
}
129

ArrayList의 get 메서드를 이용하면 특정 인덱스의 값을 추출할 수 있다.

size

size 메서드는 ArrayList의 갯수를 리턴한다.

System.out.println(pitches.size());
3

위 코드를 실행하면 pitches에 담긴 갯수가 출력된다.

contains

contains 메서드는 리스트 안에 해당 항목이 있는지를 판별하여 그 결과를 boolean으로 리턴한다.

System.out.println(pitches.contains("142"));
true

pitches 객체는 142 값을 포함하고 있으므로 true가 출력된다.

remove

remove 메서드에는 2가지 방식이 있다. (이름은 같지만 입력파라미터가 다르다)

  1. remove(객체)
  2. remove(인덱스)

첫번째 remove(객체)의 경우는 리스트에서 객체에 해당되는 항목을 삭제하고 삭제한 결과(true, false)를 리턴한다.

System.out.println(pitches.remove("129"));
true

"129"라는 항목이 성공적으로 삭제되고 true를 리턴한다.

두 번째 remove(인덱스)의 경우는 해당 인덱스의 항목을 삭제하고 삭제된 항목을 리턴한다.

System.out.println(pitches.remove(0));
138

pitches의 첫번째 항목은 "138"이므로 "138"이 삭제된 후 "138"을 리턴한다.

제네릭스(Generics)

제네릭스(Generics)는 자바 J2SE 5.0 이후에 도입된 개념이다. 여기서는 제네릭스를 사용하는 방법에 대해서만 다룬다. 제네릭스를 만드는 방법은 입문서에 어울리지 않는 고급 주제이므로 이 책에서는 생략한다.

다음과 같은 것이 제네릭스이다.

ArrayList<String> pitches = new ArrayList<String>();

하지만 보통 뒷 부분의 자료형은 굳이 적지 않아도 의미가 명확하기 때문에 다음의 표기법을 사용하는 것이 좋다.

ArrayList<String> pitches = new ArrayList<>();  // 선호되는 방식

만약 앞 뒤에 모두 자료형 타입을 적을 경우 인텔리제이와 같은 IDE에서는 경고메시지를 출력한다.

제네릭스가 도입되기 전인 J2SE 1.4 까지는 위의 코드를 다음과 같이 사용했다.

ArrayList pitches = new ArrayList();

두 개 코드의 차이점은 ArrayList 라는 자료형 타입 바로 옆에 <String> 과 같은 문구가 있느냐 없느냐의 차이이다.

JSSE 5.0 이후 버전에서 제네릭스가 아닌 방법으로 코드를 작성해도 오류는 나지 않는다. 다만 제네릭스 타입을 명확하게 지정하라는 Warning 메시지가 출력된다.

위에서 사용한 첫번째 코드의 <String> 이라는 제네릭스 표현식은 "ArrayList 안에 담을 수 있는 자료형은 String 타입 뿐이다" 라는 것을 의미한다. 즉, 제네릭스를 이용하면 좀 더 명확한 타입체크가 가능해 진다. 이것은 코드를 작성할 때도 몇 가지 이득을 준다.

우선 제네릭스를 사용하지 않은 경우를 생각해 보자.

ArrayList pitches = new ArrayList();
pitches.add("138");
pitches.add("129");

String one = (String) pitches.get(0);
String two = (String) pitches.get(1);

위처럼 제네릭스를 사용하지 않을 경우에는 ArrayList 안에 추가되는 객체는 Object 자료형으로 인식된다. Object 자료형은 모든 객체가 상속하고 있는 가장 기본적인 자료형이다. 따라서 ArrayList 객체인 pitches에 값을 넣을 때는 문제가 안되지만 값을 가져올 경우에는 항상 Object 자료형에서 String 자료형으로 다음과 같이 형변환(casting)을 해야 한다.

String one = (String) pitches.get(0); // Object 자료형을 String 자료형으로 캐스팅한다.

그런데 여기서 주의할 점은 pitches 안에는 String 객체 이외의 객체도 넣을 수 있기 때문에 형 변환 과정에서 잘못된 형변환으로 인한 오류가 발생할 가능성이 있다는 점이다. 바로 이러한 점이 제네릭스의 탄생 이유이기도 하다.

위 코드를 제네릭스를 사용하도록 변경해 보자.

ArrayList<String> pitches = new ArrayList<>();
pitches.add("138");
pitches.add("129");

String one = pitches.get(0);  // 형 변환이 필요없다.
String two = pitches.get(1);  // 형 변환이 필요없다.

제네릭스로 자료형을 선언하기만 하면 그 이후로는 자료형에 대한 형변환 과정이 필요없다. 이미 컴파일러가 pitches에는 반드시 String 자료형만 추가 되어야 함을 알기 때문이다. 이렇게 제네릭스를 이용하면 형변환에 의한 불필요한 코딩과 잘못된 형변환에 의한 런타임 오류를 방지할 수 있다.

다양한 방법으로 ArrayList 만들기

ArrayList의 add 메서드를 사용하면 다음처럼 ArrayList 객체에 요소를 추가할 수 있다.

import java.util.ArrayList;

public class Sample {
    public static void main(String[] args) {
        ArrayList<String> pitches = new ArrayList<>();  // 제네릭스를 사용한 표현
        pitches.add("138");
        pitches.add("129");
        pitches.add("142");
        System.out.println(pitches);  // [138, 129, 142] 출력
    }
}
[138, 129, 142]

하지만 다음처럼 이미 문자열 배열이 존재할 경우에는 보다 편하게 ArrayList를 생성할 수 있다.

import java.util.ArrayList;
import java.util.Arrays;

public class Sample {
    public static void main(String[] args) {
        String[] data = {"138", "129", "142"};  // 이미 투구수 데이터 배열이 있다.
        ArrayList<String> pitches = new ArrayList<>(Arrays.asList(data));
        System.out.println(pitches);  // [138, 129, 142] 출력
    }
}
[138, 129, 142]

java.util.Arrays 클래스의 asList 메서드를 사용하면 이미 존재하는 문자열 배열로 ArrayList를 생성할 수 있다.

또는 다음과 같이 String 배열 대신 String 자료형을 여러개 전달하여 생성할 수도 있다.

import java.util.ArrayList;
import java.util.Arrays;

public class Sample {
    public static void main(String[] args) {
        ArrayList<String> pitches = new ArrayList<>(Arrays.asList("138", "129", "142"));
        System.out.println(pitches);
    }
}
[138, 129, 142]

String.join

앞에서 "138", "129", "142" 의 요소로 이루어진 ArrayList를 만들어 보았다. 그렇다면 이렇게 만들어진 ArrayList의 각 요소를 콤마(",")로 구분하여 다음과 같은 하나의 문자열로 만들 수 있는 방법이 있을까?

138,129,142

콤마를 각 요소 중간에 삽입하려면 다음과 같이 코드를 작성해야 한다.

import java.util.ArrayList;
import java.util.Arrays;

public class Sample {
    public static void main(String[] args) {
        ArrayList<String> pitches = new ArrayList<>(Arrays.asList("138", "129", "142"));
        String result = "";
        for (int i = 0; i < pitches.size(); i++) {
            result += pitches.get(i);
            result += ",";  // 콤마를 추가한다.
        }
        result = result.substring(0, result.length() - 1);  // 마지막 콤마는 제거한다.
        System.out.println(result);  // 138,129,142 출력
    }
}
138,129,142

위 예는 pitches를 갯수만큼 루프를 돌면서 뒤에 콤마를 더하고 최종적으로 마지막 콤마를 제거하는 방법이다. 위 예에서도 볼 수 있듯이 리스트의 각각의 요소 사이에 구분자를 끼워넣어 하나의 문자열로 만드는 것은 꽤 까다로운 일이다.

하지만 String.join을 사용하면 다음처럼 매우 간단하게 처리할 수 있다.

import java.util.ArrayList;
import java.util.Arrays;

public class Sample {
    public static void main(String[] args) {
        ArrayList<String> pitches = new ArrayList<>(Arrays.asList("138", "129", "142"));
        String result = String.join(",", pitches);
        System.out.println(result);  // 138,129,142 출력
    }
}
138,129,142

String.join("구분자", 리스트객체)와 같이 사용하여 리스트의 각 요소에 "구분자"를 삽입하여 하나의 문자열로 만들 수 있다.

String.join은 다음처럼 문자열 배열에도 사용할 수 있다.

public class Sample {
    public static void main(String[] args) {
        String[] pitches = new String[]{"138", "129", "142"};
        String result = String.join(",", pitches);
        System.out.println(result);  // 138,129,142 출력
    }
}

String.join 메서드는 Java 8 버전부터 사용할 수 있다.

리스트 정렬하기

이번에는 "138", "129", "142"의 요소로 이루어진 ArrayList를 순서대로 정렬해 보자. 순서대로 정렬하기 위해서는 다음처럼 리스트의 sort 메서드를 사용해야 한다.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

public class Sample {
    public static void main(String[] args) {
        ArrayList<String> pitches = new ArrayList<>(Arrays.asList("138", "129", "142"));
        pitches.sort(Comparator.naturalOrder());  // 오름차순으로 정렬
        System.out.println(pitches);  // [129, 138, 142] 출력
    }
}
[129, 138, 142]

sort 메서드에는 정렬기준을 파라미터로 전달해야 한다. 정렬기준에는 다음처럼 오름차순, 내림차순이 있다.

  • 오름차순(순방향) 정렬 - Comparator.naturalOrder()
  • 내림차순(역방향) 정렬 - Comparator.reverseOrder()

sort 메서드로 정렬후에 pitches를 출력하면 [129, 138, 142] 처럼 오름차순으로 정렬되어 출력되는 것을 확인할수 있다.

리스트의 sort 메서드는 Java 8 버전부터 사용할 수 있다.

03-08 맵 (Map)

"사람"을 예로 들면 누구든지 "이름" = "홍길동", "생일" = "몇 월 며칠" 등으로 구분할 수 있다. 자바의 맵(Map)은 이러한 대응관계를 쉽게 표현할 수 있게 해 주는 자료형이다. 이것은 요즘 나오는 대부분의 언어들도 갖고 있는 자료형으로 Associative array, Hash라고도 불린다.

맵(Map)은 사전(dictionary)과 비슷하다. 즉, people 이란 단어에 "사람", baseball 이라는 단어에 "야구"라는 뜻이 부합되듯이 Map은 Key와 Value를 한 쌍으로 갖는 자료형이다.

keyvalue
people 사람
baseball 야구

Map은 리스트나 배열처럼 순차적으로(sequential) 해당 요소 값을 구하지 않고 key를 통해 value를 얻는다. 맵(Map)의 가장 큰 특징이라면 key로 value를 얻어낸다는 점이다. baseball이란 단어의 뜻을 찾기 위해서 사전의 내용을 순차적으로 모두 검색하는 것이 아니라 baseball이라는 단어가 있는 곳만을 펼쳐보는 것이다.

HashMap

자바의 맵(Map)중 가장 기본적인 HashMap에 대해서 알아보자.

점프 투 자바Map 역시 List와 마찬가지로 인터페이스이다. Map 인터페이스를 구현한 Map 자료형에는 HashMap, LinkedHashMap, TreeMap 등이 있다. 인터페이스에 대해서는 객체지향 챕터에서 자세하게 다룰 것이다.

put

key와 value가 String 형태인 HashMap을 만들고 위에서 보았던 예제의 항목값들을 입력해 보자.

import java.util.HashMap;

public class Sample {
    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<>();
        map.put("people", "사람");
        map.put("baseball", "야구");
    }
}

key와 value는 위 예제에서 보듯이 put 메서드를 이용하여 추가할 수 있다.

HashMap 역시 제네릭스를 이용한다. 위의 HashMap의 제네릭스는 Key, Value 모두 String 타입이다. 따라서 Key, Value에 String 이외의 자료형은 사용할수 없다.

get

key에 해당하는 value값을 얻기 위해서는 get 메서드를 사용한다.

System.out.println(map.get("people"));  // "사람" 출력
사람

map 객체의 키(Key) "people" 에 대응하는 값(Value) "사람"이 출력된다.

점프 투 자바getOrDefault

맵의 key에 해당하는 value가 없을 경우에 get 메서드를 사용하면 다음처럼 null이 리턴된다.

System.out.println(map.get("java"));  // null 출력
null

null 대신 디폴트 값을 얻고 싶은 경우에는 getOrDefault 메서드를 사용한다.

System.out.println(map.getOrDefault("java", "자바"));  // "자바" 출력
자바

containsKey

containsKey 메서드는 맵(Map)에 해당 키(key)가 있는지를 조사하여 그 유무를 참, 거짓으로 리턴한다.

System.out.println(map.containsKey("people"));  // true 출력
true

"people"이라는 키는 존재하므로 true가 출력된다.

remove

remove 메서드는 맵(Map)의 항목을 삭제하는 메서드로 key값에 해당되는 아이템(key, value)을 삭제한 후 그 value 값을 리턴한다.

System.out.println(map.remove("people"));  // "사람" 출력
사람

"people"에 해당되는 Key, Value 쌍이 삭제된 후 "사람"이 출력된다.

size

size 메서드는 Map의 갯수를 리턴한다.

System.out.println(map.size());
1

"people", "baseball" 두 값을 가지고 있다가 "people"항목이 삭제되었으므로 1이 출력된다.

keySet

keySet은 맵(Map)의 모든 Key를 모아서 리턴한다.

import java.util.HashMap;

public class Sample {
    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<>();
        map.put("people", "사람");
        map.put("baseball", "야구");
        System.out.println(map.keySet());  // [baseball, people] 출력
    }
}
[baseball, people]

keySet() 메서드는 Map의 모든 Key를 모아서 Set 자료형으로 리턴한다. Set 자료형은 잠시후에 알아본다. Set 자료형은 다음과 같이 List 자료형으로 바꾸어 사용할수도 있다.

List<String> keyList = new ArrayList<>(map.keySet());
점프 투 자바LinkedHashMap과 TreeMap

Map의 가장 큰 특징은 순서에 의존하지 않고 key로 value를 가져오는데 있다. 하지만 가끔은 Map에 입력된 순서대로 데이터를 가져오고 싶은 경우도 있고 때로는 입력된 key에 의해 소트되도록 저장하고 싶을 수도 있을 것이다. 이런경우에는 LinkedHashMap과 TreeMap을 사용하는 것이 유리하다.

  • LinkedHashMap은 입력된 순서대로 데이터를 저장하는 특징을 가지고 있다.
  • TreeMap은 입력된 key의 오름차순 순서로 데이터를 저장하는 특징을 가지고 있다.

03-09 집합 (Set)

집합(Set) 자료형은 집합과 관련된 것을 쉽게 처리하기 위해 만든 자료형이다.

집합 자료형은 어떻게 만들까?

집합 자료형은 다음과 같이 HashSet을 사용하여 만들 수 있다.

import java.util.Arrays;
import java.util.HashSet;

public class Sample {
    public static void main(String[] args) {
        HashSet<String> set = new HashSet<>(Arrays.asList("H", "e", "l", "l", "o"));
        System.out.println(set);  //  [e, H, l, o] 출력
    }
}
[e, H, l, o] 
점프 투 자바Set 자료형

Set 자료형에는 HashSet, TreeSet, LinkedHashSet 등의 Set 인터페이스를 구현한 자료형이 있다. 여기서 말하는 Set 자료형은 인터페이스인데 인터페이스에 대해서는 뒤에서 자세히 다루도록 한다.

집합 자료형의 특징

자, 그런데 위에서 살펴본 출력 결과가 좀 이상하지 않은가? 분명 "H", "e", "l", "l", "o" 라는 문자열 배열로 HashSet 자료형을 만들었는데 출력된 자료형에는 l 문자가 하나 빠져 있고 순서도 뒤죽박죽이다. 그 이유는 집합 자료형에는 다음과 같은 2가지 큰 특징이 있기 때문이다.

  • 중복을 허용하지 않는다.
  • 순서가 없다(Unordered).

리스트나 배열은 순서가 있기(ordered) 때문에 인덱싱을 통해 자료형의 값을 얻을 수 있지만 집합 자료형은 순서가 없기(unordered) 때문에 인덱싱으로 값을 얻을 수 없다. 이는 마치 맵 자료형과 비슷하다. 맵 자료형 역시 순서가 없는 자료형이라 인덱싱을 지원하지 않는다.

중복을 허용하지 않는 집합 자료형의 특징은 자료형의 중복을 제거하기 위한 필터 역할로 종종 사용한다.

교집합, 합집합, 차집합 구하기

집합 자료형을 정말 유용하게 사용하는 경우는 교집합, 합집합, 차집합을 구할 때이다.

우선 다음과 같이 2개의 집합 자료형을 만든 후 따라 해 보자. s1은 1부터 6까지의 값을 가지게 되었고, s2는 4부터 9까지의 값을 가지게 되었다.

import java.util.Arrays;
import java.util.HashSet;

public class Sample {
    public static void main(String[] args) {
        HashSet<Integer> s1 = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5, 6));
        HashSet<Integer> s2 = new HashSet<>(Arrays.asList(4, 5, 6, 7, 8, 9));
    }
}

제네릭스로 int를 사용하고 싶은 경우에는 int의 Wrapper 클래스인 Integer를 대신 사용해야 한다.

1. 교집합

s1과 s2의 교집합을 구해 보자.

import java.util.Arrays;
import java.util.HashSet;

public class Sample {
    public static void main(String[] args) {
        HashSet<Integer> s1 = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5, 6));
        HashSet<Integer> s2 = new HashSet<>(Arrays.asList(4, 5, 6, 7, 8, 9));

        HashSet<Integer> intersection = new HashSet<>(s1);  // s1으로 intersection 생성
        intersection.retainAll(s2);  // 교집합 수행
        System.out.println(intersection);  // [4, 5, 6] 출력
    }
}
[4, 5, 6]

retainAll 메서드를 이용하면 교집합을 간단히 구할 수 있다. s1의 데이터를 유지하기 위해 s1으로 intersection이라는 HashSet 객체를 Copy하여 생성하였다. 만약 intersection 대신 s1에 retainAll 메서드를 사용하면 s1의 내용이 변경될 것이다.

retainAll 메서드로 교집합을 수행한 후 intersection을 출력하니 교집합에 해당되는 [4, 5, 6]이 출력되었다.

2. 합집합

합집합은 다음과 같이 구할 수 있다. 이때 4, 5, 6처럼 중복해서 포함된 값은 한 개씩만 표현된다.

import java.util.Arrays;
import java.util.HashSet;

public class Sample {
    public static void main(String[] args) {
        HashSet<Integer> s1 = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5, 6));
        HashSet<Integer> s2 = new HashSet<>(Arrays.asList(4, 5, 6, 7, 8, 9));

        HashSet<Integer> union = new HashSet<>(s1);  // s1으로 union 생성
        union.addAll(s2); // 합집합 수행
        System.out.println(union);  // [1, 2, 3, 4, 5, 6, 7, 8, 9] 출력
    }
}
[1, 2, 3, 4, 5, 6, 7, 8, 9]

합집합은 addAll 메서드를 사용하여 구할 수 있다. 합집합의 결과로 [1, 2, 3, 4, 5, 6, 7, 8, 9]을 출력한다.

3. 차집합

차집합은 다음과 같이 구할 수 있다.

import java.util.Arrays;
import java.util.HashSet;

public class Sample {
    public static void main(String[] args) {
        HashSet<Integer> s1 = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5, 6));
        HashSet<Integer> s2 = new HashSet<>(Arrays.asList(4, 5, 6, 7, 8, 9));

        HashSet<Integer> substract = new HashSet<>(s1);  // s1으로 substract 생성
        substract.removeAll(s2); // 차집합 수행
        System.out.println(substract);  // [1, 2, 3] 출력
    }
}
[1, 2, 3]

차집합은 removeAll 메서드를 사용하여 구할수 있다. 차집합의 결과로 [1, 2, 3]을 출력한다.

집합 자료형 관련 메서드

값 추가하기(add)

집합 자료형에 값을 추가할 때에는 add 메서드를 사용한다.

import java.util.HashSet;

public class Sample {
    public static void main(String[] args) {
        HashSet<String> set = new HashSet<>();
        set.add("Jump");
        set.add("To");
        set.add("Java");
        System.out.println(set);  // [Java, To, Jump] 출력
    }
}
[Java, To, Jump]

값 여러 개 추가하기(addAll)

여러 개의 값을 한꺼번에 추가할 때는 다음과 같이 addAll 메서드를 사용한다.

import java.util.Arrays;
import java.util.HashSet;

public class Sample {
    public static void main(String[] args) {
        HashSet<String> set = new HashSet<>();
        set.add("Jump");
        set.addAll(Arrays.asList("To", "Java"));
        System.out.println(set);  // [Java, To, Jump] 출력
    }
}
[Java, To, Jump]

합집합을 구할 때도 addAll을 사용했다.

특정 값 제거하기(remove)

특정 값을 제거하고 싶을 때는 다음과 같이 remove 메서드를 사용한다.

import java.util.Arrays;
import java.util.HashSet;

public class Sample {
    public static void main(String[] args) {
        HashSet<String> set = new HashSet<>(Arrays.asList("Jump", "To", "Java"));
        set.remove("To");
        System.out.println(set);  // [Java, Jump] 출력
    }
}
[Java, Jump]
점프 투 자바TreeSet과 LinkedHashSet

Set 자료형은 순서가 없다는 특징이 있다. 하지만 가끔은 Set에 입력된 순서대로 데이터를 가져오고 싶은 경우도 있고 때로는 오름차순으로 정렬된 데이터를 가져오고 싶을 수도 있을 것이다. 이런경우에는 TreeSet과 LinkedHashSet을 사용한다.

  • TreeSet - 오름차순으로 값을 정렬하여 저장하는 특징이 있다.
  • LinkedHashSet - 입력한 순서대로 값을 정렬하여 저장하는 특징이 있다.

 

final

자바의 final은 자료형에 값을 단 한번만 설정할수 있게 강제하는 키워드이다. 즉, 값을 한번 설정하면 그 값을 다시 설정할 수 없다는 말이다.

public class Sample {
    public static void main(String[] args) {
        final int n = 123;  // final 로 설정하면 값을 바꿀수 없다.
        n = 456;  // 컴파일 에러 발생
    }
}

리스트의 경우도 final로 선언시 재할당은 불가능하다.

import java.util.ArrayList;
import java.util.Arrays;

public class Sample {
    public static void main(String[] args) {
        final ArrayList<String> a = new ArrayList<>(Arrays.asList("a", "b"));
        a = new ArrayList<>(Arrays.asList("c", "d"));  // 컴파일 에러 발생
    }
}

final은 프로그램 수행 도중 그 값이 변경되면 안되는 상황에 사용한다.

점프 투 자바Unmodifiable List

리스트의 경우 final로 선언시 리스트에 값을 더하거나(add) 빼는(remove) 것은 가능하다. 다만 재할당만 불가능할 뿐이다. 만약 그 값을 더하거나 빼는 것도 불가능하게 하고 싶은 경우에는 List.of로 수정이 불가능한 리스트(Unmodifiable List)를 사용해야 한다.

Copyimport java.util.List;

public class Sample {
    public static void main(String[] args) {
        final List<String> a = List.of("a", "b");
        a.add("c");  // UnsupportedOperationException 발생
    }
}

 객체지향 프로그래밍이란?

여러분 모두 계산기를 사용해 보았을 것이다. 계산기에 숫자 3을 입력하고 + 기호를 입력한 후 4를 입력하면 결괏값으로 7을 보여 준다. 다시 한 번 + 기호를 입력한 후 3을 입력하면 기존 결괏값 7에 3을 더해 10을 보여 준다. 즉 계산기는 이전에 계산한 결괏값을 항상 메모리 어딘가에 저장하고 있어야 한다.

계산기는 이전에 계산한 결괏값을 기억하고 있어야 한다.

이런 내용을 자바 코드로 구현해 보자. 계산기의 "더하기" 기능을 구현한 코드는 다음과 같다.

여기서 다루는 코드들을 아직은 이해하지 못해도 좋다. 곧 자세하게 배울 것이다. 여기에서는 객체지향에 대한 개념만 이해하면 된다.

class Calculator {
    static int result = 0;

    static int add(int num) {
        result += num;
        return result;
    }
}


public class Sample {
    public static void main(String[] args) {
        System.out.println(Calculator.add(3));
        System.out.println(Calculator.add(4));
    }
}

add 메서드는 매개변수 num으로 받은 값을 이전에 계산한 결괏값에 더한 후 돌려주는 메서드이다. 이전에 계산한 결괏값을 유지하기 위해서 result 전역 변수(static)를 사용했다.

static 키워드는 조금 후에 자세하게 공부한다. 여기서는 변수에 저장된 값을 계속 유지하기 위해 사용되었다.

프로그램을 실행하면 예상한 대로 다음과 같은 결괏값이 출력된다.

3 
7

그런데 만일 Sample 클래스에서 2대의 계산기가 필요한 상황이 발생하면 어떻게 해야 할까? 각 계산기는 각각의 결괏값을 유지해야 하기 때문에 위와 같이 Calculator 클래스 하나만으로는 결괏값을 따로 유지할 수 없다.

이런 상황을 해결하려면 다음과 같이 클래스를 각각 따로 만들어야 한다.

class Calculator1 {
    static int result = 0;

    static int add(int num) {
        result += num;
        return result;
    }
}

class Calculator2 {
    static int result = 0;

    static int add(int num) {
        result += num;
        return result;
    }
}


public class Sample {
    public static void main(String[] args) {
        System.out.println(Calculator1.add(3));
        System.out.println(Calculator1.add(4));

        System.out.println(Calculator2.add(3));
        System.out.println(Calculator2.add(7));
    }
}

똑같은 일을 하는 Calculator1과 Calculator2 클래스를 만들었다.

결괏값은 다음과 같이 의도한 대로 출력된다.

3
7
3
10

계산기 1의 결괏값이 계산기 2에 아무 영향을 끼치지 않음을 확인할 수 있다. 하지만 계산기가 3개, 5개, 10개로 점점 더 많이 필요해진다면 어떻게 해야 할까? 그때마다 클래스를 추가할 것인가? 여기에 빼기나 곱하기 등의 기능을 추가해야 한다면 상황은 점점 더 어려워질 것이다.

아직 객체에 대해 배우지 않았지만, 위와 같은 경우 객체를 사용하면 다음과 같이 간단하게 해결할 수 있다.

class Calculator {
    int result = 0;

    int add(int num) {
        result += num;
        return result;
    }
}


public class Sample {
    public static void main(String[] args) {
        Calculator cal1 = new Calculator();  // 계산기1 객체를 생성한다.
        Calculator cal2 = new Calculator();  // 계산기2 객체를 생성한다.

        System.out.println(cal1.add(3));
        System.out.println(cal1.add(4));

        System.out.println(cal2.add(3));
        System.out.println(cal2.add(7));
    }
}

Calculator 클래스의 static 키워드를 모두 삭제했다.

프로그램을 실행하면 클래스 2개를 사용했을 때와 동일한 결과가 출력된다.

3
7
3
10

Calculator 클래스로 만든 별개의 계산기 cal1, cal2(이것을 객체라고 부른다)가 각각의 역할을 수행한다. 그리고 계산기(cal1, cal2)의 결괏값 역시 다른 계산기의 결괏값과 상관없이 독립적인 값을 유지한다. 객체를 사용하면 계산기 대수가 늘어나더라도 객체를 생성만 하면 되기 때문에 앞의 경우와는 달리 매우 간단해진다.

만약 빼기 기능을 추가하려면 Calculator 클래스에 다음처럼 sub 메서드를 추가하면 된다.

class Calculator {
    int result = 0;

    int add(int num) {
        result += num;
        return result;
    }

    int sub(int num) {
        result -= num;
        return result;
    }
}

객체지향 프로그래밍의 이점은 단순히 이것만이 아니다. 하지만 이것 하나만으로도 "도대체 왜 객체가 필요한 것일까?"라는 근본적인 물음에 대한 해답이 되었을 것이다.

 

 

# 5장까지 진행, 5장이 좀 어렵네 ?

05-02 클래스

이번 장에서는 클래스에 대해서 자세히 알아보자.

여기서는 클래스에 대한 개념적인 설명을 하기보다는 가장 간단한 클래스를 작성하는 것에서부터 시작하여 조금씩 클래스의 규모를 키워가는 방식으로 클래스를 설명해 볼까 한다.

자, 시작해 보자.

객체에 대하여

Animal 클래스를 다음과 같이 Sample.java 파일에 작성하자.

Sample.java

class Animal {
}

public class Sample {
    public static void main(String[] args) {
    }
}

Animal 클래스를 Sample.java 파일에 작성한 것은 책의 원할한 설명을 위해서이다. 보통 클래스는 특별한 경우가 아니라면 파일 단위로 하나씩 작성한다. 즉, 위의 경우에 Animal 클래스는 Animal.java 파일에 단독으로 작성하는 것이 일반적인 방법이다. 하지만 이 책의 대부분의 예제는 Sample.java 파일만 작성하여 실행할 수 있도록 구성하였으니 참고하기 바란다.

위 Animal 클래스는 가장 간단한 형태의 클래스이다. 클래스의 선언만 있고 내용이 없는 껍데기뿐인 클래스이다. 하지만 이 껍데기뿐인 클래스도 아주 중요한 기능을 가지고 있다. 그 기능은 바로 객체(object)를 만드는 기능이다.

객체는 다음과 같이 만들 수 있다.

class Animal {
}

public class Sample {
    public static void main(String[] args) {
        Animal cat = new Animal();
    }
}

new 는 객체를 생성할 때 사용하는 키워드이다. 이렇게 하면 Animal 클래스의 인스턴스(instance)인 cat, 즉 Animal의 객체가 만들어진다.

점프 투 자바객체와 인스턴스

클래스에 의해서 만들어진 객체를 인스턴스라고도 한다. 그렇다면 객체와 인스턴스의 차이는 무엇일까?

이렇게 생각 해 보자. Animal cat = new Animal() 이렇게 만들어진 cat은 객체이다. 그리고 cat이라는 객체는 Animal의 인스턴스(instance) 이다. 인스턴스라는 말은 특정 객체(cat)가 어떤 클래스(Animal)의 객체인지를 관계위주로 설명할 때 사용된다. 즉, "cat은 인스턴스" 보다는 "cat은 객체"라는 표현이 "cat은 Animal의 객체" 보다는 "cat은 Animal의 인스턴스" 라는 표현이 훨씬 잘 어울린다.

클래스를 가장 잘 설명 해 주는 다음의 그림을 보자.

과자를 만드는 과자틀과 만들어진 과자들이다.

  • 과자틀 → 클래스 (Class)
  • 과자틀에 의해서 만들어진 과자들 → 객체 (Object)

과자틀은 클래스, 과자틀에 의해 만들어진 과자는 객체에 비유할 수 있다. 위 그림을 보면 클래스와 객체가 어떤 관계인지 감이 잡힐 것이다.

즉, 다음과 같이 무수히 많은 동물 객체(cat, dog, horse, ...)들을 Animal 클래스로 만들 수 있다.

Animal cat = new Animal();
Animal dog = new Animal();
Animal horse = new Animal();
...

여기서 설명한 내용이 이해되었다면 클래스는 70% 이상 알았다고 해도 과언이 아니다. 클래스는 결코 어렵지 않다.

객체 변수 (Instance variable)

Animal 이라는 껍데기 클래스를 조금 더 발전시켜 보자. Animal 클래스에 의해 만들어진 동물에 이름을 지어보자.

이제부터는 인텔리제이를 열고 같이 따라해 보자.

class Animal {
    String name;
}

public class Sample {
    public static void main(String[] args) {
        Animal cat = new Animal();
    }
}

Animal 클래스에 name 이라는 String 변수를 추가했다. 이렇게 클래스에 선언된 변수를 객체 변수 라고 한다. 객체 변수는 인스턴스 변수, 멤버 변수, 속성이라고도 한다.

클래스에 의해 생성되는 것은 객체, 그리고 그 클래스에 선언된 변수는 객체 변수라고 생각하면 쉽다.

객체 변수를 만들었으니 이제 객체 변수를 사용해 보자. 먼저 객체 변수는 변수이므로 값을 대입할 수 있을 것이다. 대입하기 전에 객체 변수는 현재 어떤 값을 가지고 있는지 먼저 출력해 보자. 객체 변수를 출력하려면 객체 변수에 어떻게 접근해야 하는지를 먼저 알아야 한다. 객체 변수는 다음과 같이 도트연산자(.)를 이용하여 접근할 수 있다.

객체.객체변수

즉, Animal cat = new Animal() 처럼 cat 이라는 객체를 생성했다면 이 cat 객체의 객체 변수 name에는 다음과 같이 접근할 수 있다.

cat.name   // 객체: cat, 객체변수: name

이제 객체 변수에 어떤 값이 대입되어 있는지 다음과 같이 출력해 보자.

class Animal {
    String name;
}

public class Sample {
    public static void main(String[] args) {
        Animal cat = new Animal();
        System.out.println(cat.name);
    }
}
null

cat.name을 출력한 결과값으로 null이 나왔다. null은 값이 할당되어 있지 않은 상태를 말한다. 객체 변수로 name 을 선언했지만 아무런 값도 대입을 하지 않았기 때문에 null 이라는 값이 출력된 것이다.

메서드

이제 객체변수에 접근하는 방법과 현재 값이 할당되어 있지 않은 상태에 대해서 알게 되었다. 이제 객체 변수에 값을 대입하는 방법에 대해서 알아보자. 객체 변수에 값을 대입하는 방법에는 여러가지가 있지만 여기서는 가장 보편적인 메서드를 이용하는 방법에 대해서 알아보자.

클래스에는 객체 변수와 더불어 메서드(Method)라는 것이 있다. 메서드는 클래스 내에 구현된 함수를 의미하는데 보통 함수라고 말하지 않고 메서드라고 말한다.

이제 메서드를 이용하여 Animal 클래스의 객체 변수인 name 에 값을 대입해 보자. 아래와 같이 setName 메서드를 추가해 보자.

class Animal {
    String name;

    public void setName(String name) {
        this.name = name;
    }
}

public class Sample {
    public static void main(String[] args) {
        Animal cat = new Animal();
        System.out.println(cat.name);
    }
}

Animal 클래스에 추가된 setName 메서드는 다음과 같은 형태의 메서드이다.

  • 입력: String name
  • 출력: void (리턴값 없음)

즉, 입력으로 name이라는 문자열을 받고 출력은 없는 형태의 메서드이다. 메서드의 입출력에 대한 자세한 내용은 다음 장에 준비되어 있다.

메서드가 도무지 이해가 되지 않는다면 다음 장을 먼저 보고 다시 돌아와도 좋다.

이번에는 setName 메서드의 내부를 살펴보자. setName 메서드는 다음의 문장을 가지고 있다.

this.name = name;

이 문장에서 this에 대해서 이해하는 것은 꽤 중요하다. 이 문장에 대한 설명은 잠시 보류하고 일단은 우선 이 메서드를 호출 하는 방법에 대해서 먼저 알아보자. 객체 변수에 접근하기 위해서 객체.변수 와 같이 도트연산자(.)로 접근할 수 있었던 것과 마찬가지로 객체가 메서드를 호출하기 위해서는 객체.메서드 로 호출해야 한다.

즉, 우리가 만든 setName메서드를 호출하려면 다음과 같이 호출해야 한다.

cat.setName("boby");

여기서 setName 메서드의 입력으로 "boby"와 같은 문자열을 전달해야 한다. 왜냐하면 setName메서드는 입력항목으로 문자열을 필요로 하기 때문이다.

setName메서드를 호출할 수 있도록 main메서드를 다음과 같이 수정해 보자.

class Animal {
    String name;

    public void setName(String name) {
        this.name = name;
    }
}

public class Sample {
    public static void main(String[] args) {
        Animal cat = new Animal();
        cat.setName("boby");  // 메서드 호출
        System.out.println(cat.name);
    }
}

이렇게 수정하면 cat.name을 출력하기 전에 setName 메서드가 먼저 호출 될 것이다.

자, 이제 아까 설명을 잠시 보류한 setName 메서드의 다음 문장을 다시 보자.

this.name = name;

main메서드에서 cat.setName("boby") 와 같이 "boby"라는 입력값으로 setName 메서드를 호출했기 때문에 setName함수의 입력항목 name에는 "boby"라는 문자열이 전달될 것이다.

따라서 setName 메서드의 this.name = name; 문장은 다음과 같이 해석되어 질 것이다.

this.name = "boby";

setName 메서드 내부에 사용된 this는 Animal 클래스에 의해서 생성된 객체를 지칭한다. 만약 Animal cat = new Animal() 과 같이 cat이라는 객체를 만들고 cat.setName("boby") 와 같이 cat객체에 의해 setName 메서드를 호출하면 setName 메서드 내부에 선언된 this는 바로 cat 객체를 지칭한다.

만약 Animal dog = new Animal()로 dog 객체를 만든 후 dog.setName("happy") 와 같이 호출한다면 setName 메서드 내부에 선언된 this는 바로 dog 객체를 가리킨다.

따라서 this.name = "boby"; 문장은 다시 다음과 같이 해석된다.

cat.name = "boby";

cat.name 과 같이 하면 객체 변수에 접근할 수 있음을 우리는 알고 있다. 객체 변수에 값을 대입하는 방법은 아주 쉽다. 그냥 변수에 값을 대입하는 것과 마찬가지 방법이다.

객체.객체변수 = 값

따라서 cat.name = "boby"라는 문장은 객체 cat의 객체변수 name에 "boby"라는 값을 대입한다.

다시 main 메서드를 다시 실행해보자. 다음과 같은 문자열이 출력되는 것을 확인할 수 있을 것이다.

boby

cat.name은 이제 null이 아니라 "boby"임을 확인할 수 있다.

객체 변수는 공유되지 않는다

이번에는 main메서드를 다음과 같이 변경 해 보자.

class Animal {
    String name;

    public void setName(String name) {
        this.name = name;
    }
}

public class Sample {
    public static void main(String[] args) {
        Animal cat = new Animal();
        cat.setName("boby");

        Animal dog = new Animal();
        dog.setName("happy");
    }
}

cat객체에는 "boby"라는 이름을 대입하고 dog객체에는 "happy"라는 이름을 대입했다.

이렇게 하면 setName 메서드에 의해 다음과 같은 문장이 두번 실행될 것이다.

cat.name = "boby";
dog.name = "happy";

이럴 경우 dog.name = "happy" 라는 문장이 나중에 수행되므로 cat.name의 값도 "happy"라는 값으로 변경되지는 않을까? Animal 클래스의 객체변수 name이 cat객체와 dog객체간 서로 공유되는 변수라면 아마도 그럴것이다.

다음과 같이 확인해 보자.

class Animal {
    String name;

    public void setName(String name) {
        this.name = name;
    }
}

public class Sample {
    public static void main(String[] args) {
        Animal cat = new Animal();
        cat.setName("boby");  // 메서드 호출

        Animal dog = new Animal();
        dog.setName("happy");

        System.out.println(cat.name);
        System.out.println(dog.name);
    }
}
boby
happy

결과를 보면 name 객체 변수는 공유되지 않는다는 것을 확인할 수 있다.

이 부분은 정말 너무너무 중요해서 강조하고 또 강조해도 지나치지 않다. 클래스에서 가장 중요한 부분은 그 뭐라해도 이 객체 변수의 값이 독립적으로 유지된다는 점이다. 사실 이 점이 바로 클래스 존재의 이유이기도 하다. 객체 지향적(Object Oriented)이라는 말의 의미도 곱씹어 보면 결국 이 객체 변수의 값이 독립적으로 유지되기 때문에 가능한 것이다.

객체 변수의 값은 공유되지 않지만 조금 후에 공부할 static을 이용하게 되면 객체 변수를 공유하도록 만들 수도 있다.

 

05-03 메서드 (Method)

다른 프로그래밍 언어에는 함수라는 것이 별도로 존재한다. 하지만 자바는 클래스를 떠나 존재하는 것은 있을 수 없기 때문에 자바의 함수는 따로 존재하지 않고 클래스 내에 존재한다.

자바는 이러한 클래스 내의 함수를 메서드라고 부른다.

보통 함수와 메서드가 공존하는 언어(예:파이썬)에서는 두 개를 구분하여 말하기도 하지만 자바는 메서드와 함수를 구분하여 말하지 않는다. 자바에서 사용되는 함수의 정확한 명칭은 메서드이다.

여기까지 공부해 오면서 메서드에 대해서 이미 알아 보았지만 이번 장에서는 좀 더 자세하게 알아보자.

메서드를 설명하기 전에 믹서기를 생각해보자. 우리는 믹서기에 과일을 넣는다. 그리고 믹서를 이용해서 과일을 갈아서 과일 쥬스를 만들어 낸다. 우리가 믹서기에 넣는 과일은 입력이 되고 과일 쥬스는 그 출력(리턴값)이 된다.

그렇다면 믹서기는 무엇인가?

믹서기가 바로 우리가 여기서 알고자 하는 메서드이다. 입력을 가지고 어떤 일을 수행한 다음에 결과물을 내어놓는 것, 이것이 메서드가 하는 일이다.

메서드를 사용하는 이유?

가끔 프로그래밍을 하다 보면 똑같은 내용을 자신이 반복해서 적고 있는 것을 발견할 때가 있다. 이 때가 바로 메서드가 필요한 때이다. 여러 번 반복해서 사용된다는 것은 언제고 또다시 사용할 만한 가치가 있는 부분이라는 뜻이다. 즉, 이러한 경우 이것을 한 뭉치로 묶어서 "어떤 입력값을 주었을 때 어떤 리턴값을 돌려준다"라는 식의 메서드를 작성하는 것이 현명한 일일 것이다.

가장 간단하지만 많은 것을 설명해 주는 다음의 메서드를 보자.

int sum(int a, int b) {
    return a+b;
}

위 메서드의 의미는 다음과 같이 정의된다.

“sum 메서드는 입력값으로 두개의 값(int 자료형 a, int 자료형 b)을 받으며 리턴값은 두 개의 입력값을 더한 값(int 자료형)이다.”

여기서 return은 메서드의 결과 값을 돌려주는 명령어이다. 직접 위의 메서드를 만들어 보고 사용해 보자.

public class Sample {
    int sum(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        int a = 3;
        int b = 4;

        Sample sample = new Sample();
        int c = sample.sum(a, b);

        System.out.println(c);
    }
}
7

위 코드는 sum메서드에 3, 4 라는 입력값을 전달하여 7이라는 값을 리턴받는 예제이다.

Sample 클래스의 main 메서드에서 Sample sample = new Sample()처럼 자기 자신의 객체를 생성했다. 이러한 방식이 좀 이상하게 보일수도 있지만 작성한 클래스를 단독으로 실행시켜 테스트할 때 자주 사용하는 방법이다.

매개변수와 인수

매개변수(parameter)와 인수(arguments)는 혼용해서 사용되는 헷갈리는 용어이므로 잘 기억해 두자. 매개변수는 메서드에 입력으로 전달된 값을 받는 변수를 의미하고 인수는 메서드를 호출할 때 전달하는 입력값을 의미한다.

public class Sample {
    int sum(int a, int b) {  // a, b 는 매개변수
        return a+b;
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        int c = sample.sum(3, 4);  // 3, 4는 인수

        System.out.println(c);
    }
}
  • 매개변수 - 메서드에 전달된 값을 저장하는 변수
  • 인수 - 메서드에 전달하는 값
점프 투 자바같은 의미를 가진 여러 가지 용어들에 주의하자

프로그래밍을 공부할 때 어려운 부분 중 하나가 용어의 혼용이라고 할 수 있다. 우리는 공부하면서 원서를 보기도 하고 누군가의 번역본을 보기도 하면서 의미는 같지만 표현이 다른 용어를 자주 만나게 된다. 한 예로 입력값을 다른 말로 메서드의 인수, 매개변수 등으로 말하기도 하고 결괏값을 출력값, 반환 값, 돌려주는 값 등으로 말하기도 한다. 이렇듯 많은 용어가 여러 가지 다른 말로 표현되지만 의미는 동일한 경우가 많다. 따라서 이런 용어를 기억해 놓아야 머리가 덜 아플 것이다.

메서드의 입력값과 리턴값

메서드는 들어온 입력값을 가지고 어떤 처리를 하여 적절한 리턴값을 돌려주는 블랙박스와 같다.

입력값 ---> 메서드(블랙박스) ----> 리턴값

메서드에 들어오는 입력값과 리턴값에 대해서 자세히 알아보도록 하자.

메서드의 구조

자바의 메서드 구조는 아래와 같다.

리턴자료형 메서드명(입력자료형1 매개변수1, 입력자료형2 매개변수2, ...) {
    ...    
    return 리턴값;  // 리턴자료형이 void 인 경우에는 return 문이 필요없다.
}

리턴자료형은 메서드 수행 후 돌려줄 값의 자료형을 의미한다. 메서드의 리턴값은 return 이라는 명령을 사용한다.

메서드는 입출력 유무에 따라 다음과 같이 4가지로 분류할 수 있다.

  • 입력과 출력이 모두 있는 메서드
  • 입력과 출력이 모두 없는 메서드
  • 입력은 없고 출력은 있는 메서드
  • 입력은 있고 출력은 없는 메서드

이것들에 대해서 자세히 알아보도록 하자.

일반적인 메서드

입력 값이 있고 리턴값이 있는 메서드가 일반적인 메서드이다.

일반적인 메서드의 전형적인 예를 보자.

int sum(int a, int b) {
    return a+b;
}

sum 메서드의 입출력 자료형은 다음과 같다.

  • 입력 값 - int 자료형 a, int 자료형 b
  • 리턴 값 - int 자료형

sum 메서드는 두 개의 입력값을 받아서 서로 더한 결과값을 돌려주는 메서드이다.

위처럼 입력값과 리턴값이 있는 메서드는 다음처럼 사용된다.

리턴값받을변수 = 객체.메서드명(입력인수1, 입력인수2, ...)

sum 메서드의 사용 예는 다음과 같다.

Sample sample = new Sample();
int result = sample.sum(3, 4);

위에서 보듯이 sample.sum(3, 4) 호출 후 리턴값을 저장하는 result의 자료형은 int 로 해야만 한다. 왜냐하면 sum 메서드의 리턴타입이 int로 되어 있기 때문이다. sum 메서드의 리턴타입은 int sum(int a, int b) { ... 에서 보듯이 메서드명(sum) 바로 좌측에 표기하도록 되어 있다.

입력값이 없는 메서드

입력값이 없는 메서드가 존재할까? 당연히 그렇다. 다음을 보자.

String say() {
    return "Hi";
}

say 메서드의 입출력 자료형은 다음과 같다.

  • 입력 값 - 없음
  • 리턴 값 - String 자료형

say라는 이름의 메서드를 만들었다. 하지만 입력 인수를 나타내는 괄호 안이 비어있다. 이렇듯 입력 인수가 없을 경우에는 괄호 안을 비워놓으면 된다.

이 메서드는 어떻게 쓸 수 있을까? 다음과 같이 따라해 보자.

public class Sample {
    String say() {
        return "Hi";
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        String a = sample.say();
        System.out.println(a);  // "Hi" 출력
    }
}

say 메서드를 쓰기 위해서는 say()처럼 괄호 안에 아무런 값도 넣지 않고 써야 한다. say 메서드는 입력값은 없지만 리턴값으로 'Hi'라는 문자열을 리턴한다. 따라서 String a = sample.say() 처럼 하면 a에는 "Hi"라는 문자열이 대입될 것이다.

즉, 입력값이 없고 리턴값만 있는 메서드는 다음과 같이 사용된다.

리턴값받을변수 = 객체.메서드명()

리턴값이 없는 메서드

리턴값이 없는 메서드 역시 존재한다. 다음의 예를 보자.

void sum(int a, int b) {
    System.out.println(a+"과 "+b+"의 합은 "+(a+b)+"입니다.");
}

위 sum 메서드의 입출력 자료형은 다음과 같다.

  • 입력 값 - int 자료형 a, int 자료형 b
  • 리턴 값 - void (없음)

리턴값이 없는 메서드는 명시적으로 리턴타입 부분에 void라고 표기한다. 리턴값이 없는 메서드는 다음과 같이 사용한다.

public class Sample {
    void sum(int a, int b) {
        System.out.println(a+"과 "+b+"의 합은 "+(a+b)+"입니다.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sum(3, 4);
    }
}

즉, 리턴값이 없는 메서드는 다음과 같이 사용된다.

객체.메서드명(입력인수1, 입력인수2, ...)

실제로 위 메서드를 호출해 보면 다음과 같은 문자열이 출력된다.

3과 4의 합은 7입니다.

아마도 여러분은 다음과 같은 질문을 할지도 모른다. "3과 4의 합은 7입니다." 라는 문장을 출력해 주었는데 왜 리턴값이 없다는 것인가? 이 부분이 초보자들이 혼동스러워 하는 부분이기도 한데 System.out.println 문은 메서드내에서 사용되어지는 문장일 뿐이다. 리턴 값은 당연히 없다. 리턴 값은 return 명령어로만 가능하다.

입력값도 리턴값도 없는 메서드

이것 역시 존재한다. 다음의 예를 보자.

void say() {
    System.out.println("Hi");
}

위 say 메서드의 입출력 자료형은 다음과 같다.

  • 입력 값 - 없음
  • 리턴 값 - void (없음)

이 메서드를 사용하는 방법은 단 한가지이다.

public class Sample {
    void say() {
        System.out.println("Hi");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.say();
    }
}

즉, 입력값도 리턴값도 없는 메서드는 다음과 같이 사용한다.

객체.메서드명()

return의 또 다른 쓰임새

특별한 경우에 메서드를 빠져나가고 싶다면 return을 단독으로 사용하여 메서드를 즉시 빠져나갈 수 있다. 다음 예를 보자.

public class Sample {
    void sayNick(String nick) {
        if ("fool".equals(nick)) {
            return;
        }
        System.out.println("나의 별명은 "+nick+" 입니다.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("angel");
        sample.sayNick("fool");  // 출력되지 않는다.
    }
}

sayNick 메서드는 입력으로 받은 문자열을 출력하는 메서드이다. 이 메서드 역시 리턴값은 없다. 문자열을 출력한다는 것과 리턴값이 있다는 것은 전혀 다른 말이다. 혼동하지 말도록 하자. 메서드의 리턴값은 오로지 return문에 의해서만 가능하다. 이 메서드는 입력값으로 "fool" 이라는 값이 들어오면 문자열을 출력하지 않고 메서드를 즉시 빠져나간다. 이렇게 메서드 수행 시 특정 조건에 따라 메서드를 즉시 빠져나가고 싶은 경우 return 문을 단독으로 사용하면 된다. 이 방법은 실제 프로그래밍에서 자주 쓰인다.

return 문만 단독으로 사용하여 메서드를 빠져나가는 이 방법은 리턴 자료형이 void인 메서드에만 해당된다. 리턴자료형이 명시되어 있는 메서드에서 return 문만 작성하면 컴파일 오류가 발생한다.

메서드 내에서 선언된 변수의 효력 범위

메서드안에서 사용하는 변수의 이름을 메서드 밖에서 사용한 이름과 동일하게 사용한다면 어떻게 될까?

아래의 예를 보자.

public class Sample {
    void varTest(int a) {
        a++;
    }

    public static void main(String[] args) {
        int a = 1;
        Sample sample = new Sample();
        sample.varTest(a);
        System.out.println(a);
    }
}

위 예제의 varTest 메서드는 입력으로 들어온 int 자료형의 값을 1만큼 증가시키는 역할을 한다.

main 메서드를 순서대로 분석해 보자.

먼저 main메서드에서 a라는 int 자료형의 변수를 생성하고 1을 대입했다. 그리고 varTest 메서드를 입력 값 a를 주어 호출했다. 그 다음에 a의 값을 출력하게 하였다. 당연히 varTest 메서드에서 a의 값을 1만큼 증가시켰으니 2가 출력되어야 할 것 같지만 프로그램을 실행시켜 보면 1이라는 결과 값이 나온다.

그 이유는 메서드에서 사용한 매개 변수는 메서드 안에서만 쓰여지는 변수이기 때문이다. 즉 public void varTest(int a) {라는 문장에서 매개변수 a는 메서드 안에서만 쓰이는 변수이지 메서드 밖의 변수 a가 아니라는 말이다.

위에서 매개변수 이름을 a로 사용한 varTest메서드는 다음처럼 매개변수 이름을 b로 사용한 varTest와 기능적으로 완전히 동일하다.

public void varTest(int b) {
    b++;
}

즉, 메서드에서 쓰이는 매개변수의 이름은 메서드 밖의 변수 이름과는 전혀 상관 없다.

하지만 varTest 입력값이 int 자료형이 아닌 객체였다면 얘기가 다르다. 객체를 메서드의 입력으로 넘기고 메서드가 객체의 속성값(객체변수 값)을 변경한다면 메서드 수행 이후에도 객체는 변경된 속성값을 유지한다. 이러한 차이가 나는 이유는 메서드에 전달하는 입력 자료형의 형태 때문인데 메서드에 값을 전달하느냐 아니면 객체를 전달하느냐에 따라 차이가 난다.

그렇다면 varTest라는 메서드를 이용해서 메서드 외부의 a의 값을 1만큼 증가시킬 수 있는 방법은 없을까?

다음과 같이 varTest 메서드와 main 메서드를 변경해 보자.

public class Sample {
    int varTest(int a) {
        a++;
        return a;
    }

    public static void main(String[] args) {
        int a = 1;
        Sample sample = new Sample();
        a = sample.varTest(a);
        System.out.println(a);
    }
}
2

해법은 위 예처럼 varTest메서드에 return문을 이용하는 방법이다. varTest 메서드는 입력으로 들어온 값을 1만큼 증가시켜 리턴한다. 따라서 a = sample.varTest(a)처럼 하면 a의 값은 다시 varTest 메서드의 리턴값으로 대입된다. (1만큼 증가된 값으로 a의 값이 변경된다.)

이번에는 아까 잠깐 언급한 객체를 넘기는 방법에 대해서 알아보자.

다음의 예를 보자.

public class Sample {

    int a;  // 객체변수 a

    void varTest(Sample sample) {
        sample.a++;
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.a = 1;
        sample.varTest(sample);
        System.out.println(sample.a);
    }
}
2

이번에는 int 자료형인 a 변수를 Sample 클래스의 객체변수로 선언했다. 그리고 varTest 메서드는 Sample 클래스의 객체를 입력받아 해당 객체의 객체변수 a의 값을 1만큼 증가시키도록 했다. 그리고 main메서드에서는 varTest메서드 호출 시 Sample 클래스의 객체인 sample을 전달하도록 수정했다.

이렇게 수정하고 프로그램을 실행시켜보면 sample객체의 객체변수 a의 값이 원래는 1이었는데 varTest 메서드 실행 후 1만큼 증가되어 2가 출력되는 것을 확인할 수 있다.

여기서 주목해야 하는 부분은 varTest 메서드의 입력 파라미터가 값이 아닌 Sample 클래스의 객체라는데 있다. 이렇게 메서드가 객체를 전달 받으면 메서드 내의 객체는 전달받은 객체 그 자체로 수행된다. 따라서 입력으로 전달받은 sample 객체의 객체변수 a의 값이 증가하게 되는 것이다.

메서드의 입력항목이 값인지 객체인지를 구별하는 기준은 입력항목의 자료형이 primitive 자료형인지 아닌지에 따라 나뉜다. int 자료형과 같은 primitive 자료형인 경우 값이 전달되고 그 이외의 경우는 객체가 전달된다.

점프 투 자바this 활용하기

위 예제에는 다음과 같은 문장이 있다.

sample.varTest(sample);

sample 객체를 이용하여 varTest라는 메서드를 호출할 경우 굳이 sample 객체를 전달할 필요가 없다. 왜냐하면 전달하지 않더라도 varTest 메서드는 this라는 키워드를 이용하여 객체에 접근할 수 있기 때문이다. this를 이용하여 varTest메서드를 수정한 버전은 다음과 같다.

Copypublic class Sample {

    int a;  // 객체변수 a

    void varTest() {
        this.a++;
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.a = 1;
        sample.varTest();
        System.out.println(sample.a);
    }
}
2

 

5-3 메소드 다시 복습한 뒤, 나머지 공부는 오늘 오후 2시부터 시작.

해야할 것: 패턴 코드 분석, 점프투자바 남은 부분 공부, 스프링부트 (따즈아 한동석), Rest.api, 프론트엔드2 강의

'UIUX Full-Stack Developer' 카테고리의 다른 글

[0630] 점프투자바 복습  (0) 2023.06.30
object / clone 개념 / class 조사 / object **  (0) 2023.06.30
[0629] 패턴 코드 분석  (0) 2023.06.29
패턴 코드 분석  (0) 2023.06.27
[0627] 풀스택 개발자  (0) 2023.06.27