728x90

# 자바의 패키지에 대해 학습하세요.


# 학습할 것

  • package 키워드
  • import 키워드
  • 클래스패스
  • CLASSPATH 환경변수
  • -classpath 옵션
  • 접근지시자

 

package 키워드


package 키워드에 앞서 패키지 자체에 대해 생각해보려 한다.

아주 쉽게 생각하면 컴퓨터에 폴더를 생각하면 된다.

실제로 자바에서 패키지 개념도 폴더와 같다.

 

조금 더 그럴싸하게? 얘기하면 클래스를 묶은 단위로 사용할 수 있다 할 수 있다.

그러면 왜 클래스를 굳이 패키지라는 폴더와 같은 개념을 사용해서 묶도록 했을까.

 

크게 두가지 이유가 있다.

하나는 같은 이름의 클래스를 선언할 때 구분할 수 있기 때문도 있고

다른 하나는 이 이유의 연장선에서 비슷한 또는 연관있는 클래스끼리 하나의 폴더로 묶어 관리하기 위함이다.

 

패키지 이름으로 아무것이나 사용할 수 있는건 아니다.

반드시 지켜야 하는 규칙도 있지만, 그렇지 않아도 되는 규칙이 있어서 혼란스러운 경우가 종종 있지만 보통은 다음과 같은 규칙이 있다.

 

- 영문 소문자를 사용

- 상위 패키지와 하위 패키지는 . 을 사용해서 연결

- 자바 예약어는 사용하지 않음

- java 로 시작하면 자바에서 기본적으로 제공하는 패키지

- javax 로 시작하면 자바에서 기본적으로 제공하는 확장 패키지

- org 로 시작하면 비영리 단체에서 만든 패키지

- com 으로 시작하면 기업에서 만든 패키지

 

일반적으로 위와 같은 규칙이 있으니, 사용하는 클래스가 어떤 패키지에 속해 있는지 보면 대략적인 정보에 대해 알 수 있다.

 

 

패키지를 선언하지 않는 경우도 있다.

권장하지 않지만 선언하지 않는다고 해서 프로그램이 동작하지 않는것은 아니다.

이런 경우 클래스가 default 패키지에 생성 되었다고 한다.

 

 

 

public class Exam_001_Default_Package {
    public static void main(String[] args) {
        // default package 를 사용한 경우로, package 키워드를 사용하지 않는다.
    }
}

 

 

728x90

 

 

import 키워드


이 키워드는 다른 패키지에 있는 클래스를 가져다 사용하고 싶을 때 쓰는 키워드 이다.

다음은 import 키워드를 사용한 예이다.

 

 

package me.xxxelppa.study.week07.sub_01;
 
public class Exam_001_sub_01 {
    private String sub_01_str = "sub_01 패키지 안에 선언된 문자열";
    
    public String getSub_01_str() {
        return sub_01_str;
    }
}

 

package me.xxxelppa.study.week07.sub_02;
 
import me.xxxelppa.study.week07.sub_01.Exam_001_sub_01;
 
public class Exam_001_sub_02 {
    public static void main(String[] args) {
        Exam_001_sub_01 exam_001_sub_01 = new Exam_001_sub_01();
        System.out.println(":: 다른 패키지 사용 -> " + exam_001_sub_01.getSub_01_str());
    }
}

 

:: 다른 패키지 사용 -> sub_01 패키지 안에 선언된 문자열

 

import 키워드를 사용하면 static import 라는 것을 보게 된다.

static 키워드를 사용하여 멤버 필드 (클래스 레벨의 변수) 또는 메소드를 선언하면, 객체 (또는 인스턴스)를 통하지 않고 클래스 이름을 통해 사용할 수 있다.

 

즉, 다음과 같은 것이 가능하다.

 

 

package me.xxxelppa.study.week07.sub_01;
 
public class Exam_002_sub_01_static {
    public static void print() {
        System.out.println("sub_01 패키지에 있는 static 메소드 print 사용");
    }
}

 

package me.xxxelppa.study.week07.sub_02;
 
import static me.xxxelppa.study.week07.sub_01.Exam_002_sub_01_static.print;
 
public class Exam_002_sub_02_static {
    public static void main(String[] args) {
        print();
    }
}

 

sub_01 패키지에 있는 static 메소드 print 사용

 

만약 static import 를 사용하지 않았다면, 7라인에서 print 메소드를 사용하기 위해

다음과 같이 작성 했어야 했다.

 

package me.xxxelppa.study.week07.sub_02;
 
import me.xxxelppa.study.week07.sub_01.Exam_002_sub_01_static;
 
public class Exam_002_sub_02_static {
    public static void main(String[] args) {
        Exam_002_sub_01_static.print();
    }
}

 

 

 

클래스 패스, CLASSPATH 환경변수


JVM에 의해 프로그램이 실행될 때 사용할 클래스 파일들을 찾는 경로를 클래스패스 라고 한다.

클래스패스를 설정하는 방법이 몇가지 있는데, 윈도우 기준으로 윈도우 시스템에서 사용 할 클래스 패스를 환경변수에 등록하는 방법과

ide와 같은 통합 개발 환경을 제공하는 도구에 classpath 를 설정하여 사용하는 방법이 있다.

 

예전에 환경변수를 설정하고 클래스패스에 대해 정리했던 글이다.

 

 

 

 

-classpath 옵션


이 옵션에 대해서는 윈도우 기준 명령 프롬프트에 java -help 명령을 입력하면 설명이 나와있다.

 

-classpath

class search path of directories and zip/jar files

 

--class-path

class search path of directories and sip/jar files

A ; separated list of directories, JAR archives, and ZIP archives to search for class files.

 

 

실행이 아니라 컴파일을 할 경우에는 다음과 같이 사용할 수 있다.

 

 

 

접근지시자


접근지시자는 다른 말로 접근제한자 라고도 한다.

클래스나 멤버 필드 또는 클래스 내부에 선언 된 메소드에 대해 외부의 접근을 어디까지 허용할 것이지 정할 때 사용할 수 있다.

 

5주차 과제에서 한 번 언급한 적이 있기 때문에 간략하게 표로 정리해 보았다.

 

구분 적용 가능한 접근지시자
class (클래스) public 또는 생략 (default)
멤버 필드 (클래스 변수) public, protected, 생략 (default), private
멤버 메소드
지역 변수 (메소드 변수)  

 

public : 자신을 포함한 모든 위치에서 사용이 가능하다. 즉, 제한이 없다.

protected : 같은 패키지 또는 상속 관계에서 접근 해서 사용할 수 있다.

생략(default) : package 라는 이름으로 부르기도 하는데, 같은 패키지 내에서 접근할 수 있는 지시자 또는 제한자 이다.

private : 같은 클래스 내에서만 접근해서 사용할 수 있다.

 

 

 

 

728x90

'Archive > Java online live study S01' 카테고리의 다른 글

9주차 : 예외 처리  (0) 2021.05.01
8주차 : 인터페이스  (0) 2021.05.01
6주차 : 상속  (0) 2021.05.01
5주차 : 클래스  (0) 2021.05.01
4주차 : 제어문  (0) 2021.05.01
728x90

# 자바의 상속에 대해 학습하세요.


# 학습할 것

  • 자바 상속의 특징
  • super 키워드
  • 메소드 오버라이딩
  • 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
  • 추상 클래스
  • final 키워드
  • Object 클래스

 

자바 상속의 특징


상속은 상속을 해주는 클래스와 상속을 받는 클래스 두 클래스 사이에서 일어날 수 있는 일이다.

용어에서도 알 수 있듯, 상속을 해주는 클래스가 가진 멤버 필드(클래스 레벨의 변수)와 멤버 메소드를 상속을 받는 클래스에서 마치 자신의 것처럼 사용할 수 있는 것을 뜻한다.

 

그럼 왜 상속이라는 개념을 도입 했을까?

자바에 대해 개인적으로 느끼고 있는 것이지만, 상수, 변수, 배열, 반복문, 클래스, 제네릭 등 대부분의 모든 개념들은 ​'중복을 피하고 싶다'​라는 강력한 귀차니즘을 해소하기 위한 결과물이라고 생각 한다. (태양 말고 중복을 피하고 싶어서...)

 

솔직히 반복문이 없어도 복사, 붙여넣기를 열심히 하면 없어도 된다.

그런데 왜 굳이 반복문을 사용할까.

중복 코드를 피한다는 것을 제외 하더라도, 보다 읽기 좋은 코드를 작성해서 유지보수 하기 좋게 하기 위함도 있다.

상속도 마찬가지이다.

특정 클래스를 상속 받은 클래스들은, 상속 해주는 클래스가 가진 것들을 기본적으로 가지고 있다는 것을 뜻하기 때문에

​수정하고자 하는 속성이나 기능이 이 클래스에 있다면, 이 클래스 한 곳만 수정하면 모든 클래스에 쉽게 적용​할 수 있기 때문이다.

그 외에 다른 이점들도 많이 있지만, 우선 중복 코드를 제거하고 유지보수성을 높이기 위함이 있다는 것을 기억했으면 한다. 

 

** 앞으로 상속을 해주는 클래스를 부모 클래스(superclass), 상속을 받는 클래스를 자식 클래스(subclass) 라고 하겠다.

 

자바에서의 상속이 가지는 몇가지 특징이 있다.

1. 인터페이스를 제외한 부모 클래스를 단 하나만 가질 수 있다.

2. 부모 클래스를 가진 자식 클래스도 다른 클래스의 부모 클래스가 될 수 있다.

3. 모든 클래스는 Object 라는 클래스를 암묵적으로 상속 받고 있다.

4. 모든 클래스의 가장 위에 있는 부모 클래스는 Object 라는 클래스 이다.

 

(위에서 인터페이스라는 말을 사용 했는데, 8주차에 알아보려 한다.)

 

하나의 부모 클래스는 다수의 자식 클래스를 가질 수 있지만, 반대로 하나의 자식 클래스는 둘 이상의 부모 클래스를 가질 수 없다.

그 이유는 다음 그림을 보자.

 

 

우선 위와 같은 모양으로 코드 작성을 시도하면 "Class cannot extend multiple classes" 컴파일 오류가 발생한다.

하지만 만약 가능하다는 가정하에 위와 같이 작성했다면

자식 클래스에서 부모 클래스의 걸어간다() 를 사용할 경우 동쪽으로 갈지 서쪽으로 갈지 알 수 없다.

이것을 다중 상속이라고 한다.

 

즉, 위와 같은 문제가 있기 때문에 자바에서는 클래스의 다중 상속을 지원하지 않기 때문에 상속을 받을 때는 신중해야 한다.

한 번 상속 받으면 다른 클래스를 상속 받을 수 없기 때문이다.

 

 

이 상속과 관련해서 한가지 재미있는게 있다.

앞서 클래스에 대해 정리했을 때, 클래스는 사용자가 정의한 자료형이라고 표현했다.

다시 말해서 데이터 타입으로 취급할 수 있다는 것인데, 같은 데이터 타입을 사용할 때 배열이라는 것을 사용 했다.

이 말은 클래스도 배열에 담아 사용할 수 있다는 것을 뜻하는데, 다음과 같은 코드를 작성할 수 있다.

 

package me.xxxelppa.study.week06;
 
public class Exam_001 {
    public static void main(String[] args) {
        MyType[] mt = new MyType[10];
        
        for(int i = 0; i < mt.length; ++i) {
            mt[i] = new MyType();
        }
    }
}
 
class MyType {}

 

mt 배열은 10개의 MyType 클래스 타입의 변수(또는 객체)를 요소로 가질 수 있는 배열이다.

 

 

갑자기 이 코드를 작성한 이유는, 상속에서 자식은 자기 자신의 타입이기도 하지만 ​동시에 부모 타입​이기도 하기 때문이다.

코드를 보면 쉽게 이해할 수 있다.

 

package me.xxxelppa.study.week06;
 
public class Exam_002 {
    public static void main(String[] args) {
        MyParent mp = new MyChild();  // 부모 타입에 자식 타입 객체를 할당
    
        MyParent[] arrMP = new MyParent[10];
        for(int i = 0; i < arrMP.length; ++i) {
            if(i % 2 == 0) arrMP[i] = new MyChild();  // 짝수번째 요소에는 자식 객체
            else arrMP[i] = new MyParent();           // 홀수번째 요소에는 부모 객체
        }
    }
}
 
class MyParent { }
class MyChild extends MyParent { }

 

MyChild 입장에서 MyParent 는 부모 클래스이다.

그리고 자식 타입은 부모 타입의 변수에 할당될 수 있기 때문에 위와 같은 코드가 문제 없이 동작한다.

이것을 ​다형성​이라고 한다.

 

자바 언어를 사용 하면서 매우 아주 엄청 진짜 뻥안치고 중요한 내용이기 때문에 정말 잘 알아둬야 한다.

상속을 사용하지 않는다는 것은 포인터를 사용하지 않고 C언어를 사용하는 것과 다를바 없다.

 

 

728x90

 

 

 

super 키워드


this 키워드가 자기 자신의 클래스를 나타내는 키워드 였다면, super는 상속 관계에서 부모 클래스를 가리키는 키워드 이다.

코드를 보는게 이해가 빠를것 같다.

 

package me.xxxelppa.study.week06;
 
public class Exam_003 extends SuperTestClass {
    public static void main(String[] args) {
        Exam_003 exam_003 = new Exam_003();
        exam_003.myPrint();
        System.out.println();
    
        System.out.println("어떤 결과 ?");
        exam_003.print();
    }
    
    public void myPrint() {
        super.print();  // 상속 관계에서 부모 클래스에 접근
        print();
    }
    
    public void print() {
        System.out.println("자식 클래스 메소드");
    }
}
 
class SuperTestClass {
    public void print() {
        System.out.println("부모 클래스의 메소드");
    }
}

 

부모 클래스의 메소드
자식 클래스 메소드

어떤 결과?
자식 클래스 메소드

 

현재 부모 클래스에도 자식 클래스에도 모두 print 라는 메소드가 모두 존재한다.

이 때 14라인에서 명시적으로 부모 클래스의 print 메소드를 실행 하라고 했기 때문에, 자식 클래스에 있는 print 를 사용하지 않고 부모 클래스의 메소드를 사용한 것을 알 수 있다.

 

마지막으로 super 키워드는 this 키워드와 마찬가지로 super() 라는 것이 있다.

this()가 자기 자신 클래스의 다른 생성자를 호출 했다면, super() 는 부모 클래스의 생성자를 호출 하는데 사용한다.

 

package me.xxxelppa.study.week06;
 
public class Exam_004 extends SuperClass {
    public Exam_004() {
    }
    
    public static void main(String[] args) {
        Exam_004 exam_004 = new Exam_004();
    }
}
 
class SuperClass {
    public SuperClass() {
        System.out.println("부모 클래스의 기본 생성자");
    }
}

 

부모 클래스의 기본 생성자

 

자바에서는 객체를 생성하면, 그 클래스의 부모 클래스의 기본 생성자를 자동으로 실행하도록 되어 있다.

그렇기 때문에 위에 작성한 예제 코드에서 super() 를 작성하지 않았지만,

Exam_004() 생성자 안에서 super() 가 있는 것처럼 동작한 것을 확인할 수 있다.

 

this 에서처럼 매개변수를 넣어주면, 해당 매개변수를 갖는 부모 클래스의 생성자가 호출 된다.

 

 

 

메소드 오버라이딩


메소드 오버라이딩은 다른 말로 '메소드 재정의' 라고 한다.

이런 표현은 잘 쓰지 않지만, 덮어쓰기라고 생각하면 될 것 같다.

 

상속 관계에서 부모 클래스의 메소드를 자신의 것처럼 가져다 사용할 수 있는데

그 내용이 마음에 들지 않거나(?) 다시 정의해서 사용해야 할 필요가 있을 경우 메소드 오버라이딩을 할 수 있다.

 

package me.xxxelppa.study.week06;
 
public class Exam_005 extends OverrideTestClass {
    public static void main(String[] args) {
        Exam_005 exam_005 = new Exam_005();
        exam_005.myPrint();
    }
    
    @Override
    public void myPrint() {
        System.out.println("자식 클래스에서 오버리이딩 한 메소드를 실행 합니다.");
    }
}
 
class OverrideTestClass {
    public void myPrint() {
        System.out.println("부모 클래스의 myPrint 메소드를 실행 합니다.");
    }
}

 

자식 클래스에서 오버라이딩 한 메소드를 실행 합니다.

 

그리고 굳이 작성해주지 않아도 괜찮지만, 굳이 작성해주면 좋은 @Override 어노테이션이 있다.

어노테이션은 @ 를 사용하여 나타내는데, 다양하게 사용할 수 있지만 보통 메타 데이터로 사용 된다.

메타 데이터란 데이터를 위한 데이터라고도 하며, 해당 데이터가 어떤 데이터인지 설명해주는 것이다.

 

지금 코드에서 사용한 @Override 라는 어노테이션은

public void myPrint() 메소드가 Override 한 메소드 라는 것을 컴파일러에게 알려주는 역할을 한다.

 

만약, 부모 클래스에 재정의 할 메소드가 없을 경우 컴파일 시점에 오류를 발생해 주기 때문에 런타임시 오작동을 방지할 수 있다.

위 코드에서 OverrideTestClass 클래스의 myPrint 메소드 이름을 다른 것으로 바꾸면

9라인에 "Method does not override method from its superclass" 라는 컴파일 오류가 발생하는 것을 볼 수 있다.

 

이렇게 컴파일 시점에 사용자의 typo를 검출 하기도 하지만, 코드를 읽는 사람이 '이 메소드는 오버라이딩 된 것이구나' 라고 바로 알 수 있기 때문에 가독성에도 이점이 있다.

 

 

그럼 만약 내가 오버라이드 했지만 그럼에도 불구하고 부모의 원래 메소드를 사용하고 싶을 때가 있을수도 있다.

이런 경우 어떻게 해야 할까?

 

해법은 이미 알고 있다. super 키워드를 사용해서 호출하면 된다.

 

 

 

다이나믹 메소드 디스패치 (Dynamic Method Dispatch)


메소드 디스패치는 크게 두가지 타입이 존재한다.

1. 스태틱 (정적)

2. 다이나믹 (동적)

 

컴파일 타임에 호출 할 메소드를 알 수 있으면 정적

컴파일 타임이 아닌 런타임에 호출 할 메소드를 알 수 있으면 동적 이라고 한다.

 

동적 디스패치의 경우 다형적인 코드를 작성할 때 발생하는데, 역시 코드를 보는게 좋을것 같다.

 

package me.xxxelppa.study.week06;
 
public class Exam_008 {
    public static void main(String[] args) {
        // 정적 디스패치
        Child_01 child_01 = new Child_01();
        child_01.print();
        Child_02 child_02 = new Child_02();
        child_02.print();
        System.out.println();
        
        // 동적 디스패치 (오버라이딩)
        Super dynamic_dispatch_01 = new Child_01();
        dynamic_dispatch_01.print();
        Super dynamic_dispatch_02 = new Child_02();
        dynamic_dispatch_02.print();
        System.out.println();
    }
}
 
class Super {
    public void print() {
        System.out.println("부모 클래스의 메소드");
    }
}
 
class Child_01 extends Super {
    @Override
    public void print() {
        System.out.println("자식 01 에서 재정의 한 메소드");
    }
}
 
class Child_02 extends Super {
    @Override
    public void print() {
        System.out.println("자식 02 에서 재정의 한 메소드");
    }
}

 

자식 01 에서 재정의 한 메소드
자식 02 에서 재정의 한 메소드

자식 01 에서 재정의 한 메소드
자식 02 에서 재정의 한 메소드

 

코드를 봐서는 잘 알기 어렵기 때문에 컴파일 된 바이트 코드를 열어보았다.

여기서 눈여겨 볼 부분은 소스 코드 상의 7, 9, 14, 16 라인 이므로 해당 부분을 굵은 글씨로 수정했다.

// class version 52.0 (52)
// access flags 0x21
public class me/xxxelppa/study/week06/Exam_008 {


  // compiled from: Exam_008.java


  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lme/xxxelppa/study/week06/Exam_008; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1


  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 6 L0
    NEW me/xxxelppa/study/week06/Child_01
    DUP
    INVOKESPECIAL me/xxxelppa/study/week06/Child_01.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 7 L1
    ALOAD 1
    INVOKEVIRTUAL me/xxxelppa/study/week06/Child_01.print ()V
   L2
    LINENUMBER 8 L2
    NEW me/xxxelppa/study/week06/Child_02
    DUP
    INVOKESPECIAL me/xxxelppa/study/week06/Child_02.<init> ()V
    ASTORE 2
   L3
    LINENUMBER 9 L3
    ALOAD 2
    INVOKEVIRTUAL me/xxxelppa/study/week06/Child_02.print ()V
   L4
    LINENUMBER 10 L4
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    INVOKEVIRTUAL java/io/PrintStream.println ()V
   L5
    LINENUMBER 13 L5
    NEW me/xxxelppa/study/week06/Child_01
    DUP
    INVOKESPECIAL me/xxxelppa/study/week06/Child_01.<init> ()V
    ASTORE 3
   L6
    LINENUMBER 14 L6
    ALOAD 3
    INVOKEVIRTUAL me/xxxelppa/study/week06/Super.print ()V
   L7
    LINENUMBER 15 L7
    NEW me/xxxelppa/study/week06/Child_02
    DUP
    INVOKESPECIAL me/xxxelppa/study/week06/Child_02.<init> ()V
    ASTORE 4
   L8
    LINENUMBER 16 L8
    ALOAD 4
    INVOKEVIRTUAL me/xxxelppa/study/week06/Super.print ()V
   L9
    LINENUMBER 17 L9
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    INVOKEVIRTUAL java/io/PrintStream.println ()V
   L10
    LINENUMBER 18 L10
    RETURN
   L11
    LOCALVARIABLE args [Ljava/lang/String; L0 L11 0
    LOCALVARIABLE child_01 Lme/xxxelppa/study/week06/Child_01; L1 L11 1
    LOCALVARIABLE child_02 Lme/xxxelppa/study/week06/Child_02; L3 L11 2
    LOCALVARIABLE dynamic_dispatch_01 Lme/xxxelppa/study/week06/Super; L6 L11 3
    LOCALVARIABLE dynamic_dispatch_02 Lme/xxxelppa/study/week06/Super; L8 L11 4
    MAXSTACK = 2
    MAXLOCALS = 5
}

다형적이지 않은 7, 9 라인에서는 컴파일 결과 어떤 객체의 메소드를 사용할지 알 수 있지만

다형적인 코드를 사용한 14, 16라인에서는 부모 타입의 print 메소드를 사용 한다고 되어있고, 실제 사용되는 객체에 대한 정보는 알 수 없다.

다시 말해서 컴파일 시점이 아닌 런타임 시점에 어떤 객체의 메소드를 사용할지 결정 된다는 것인데, 이런 것을 동적 디스패치 라고 한다.

 

 

 

추상 클래스


자바를 쓰다보면 추상이라는 말을 자주 접한다. 일상에서 자주 사용하는 말이 아니어서 그 의미가 쉽게 와닿지 않을 수 있다.

추상적이라는 말의 반대 의미를 갖는 말 중에 구체적이라는 말을 보통 사용한다.

이래도 이해가 어렵다면, 공통적인 부분을 추출해낸다 라고 할 수도 있다.

 

예를 들어보자.

기린, 코끼리, 나무늘보, 고슴도치가 있다.

다양한 관점에서 해석할 수 있겠지만, 우리는 이들을 '동물' 이라고 말한다.

이렇게 공통적인 부분을 추출해내어 '동물' 이라고 한 것이 추상화 했다고 할 수 있다.

 

동물을 한번 더 추상화 해본다고 하자.

동물과 비슷한 급에서 분류 하는 것중 식물도 있다.

이것을 '생명체' 라고 할 수 있는데, 이것도 추상화 했다고 할 수 있다.

 

그 반대 방향을 구체화 했다고 한다.

 

100% 똑같지 않을 수 있지만, 약간 대분류, 중분류, 소분류 하는 것과 비슷한 느낌이다.

 

추상화라는 것이 무엇인지 느낌을 알았으니, 추상 클래스가 무엇인지 알아보자.

(추상화와 추상 클래스는 조금 다르다.)

 

 

아주 간단하게 요약하면, 추상 메소드를 가지면 그게 추상 클래스이다.

하지만 추상 클래스라고 해서 추상 메소드를 가져야만 하는 것은 아니다.

추상 메소드가 없어도 추상 클래스로 선언 했으면 그건 추상 클래스이다.

추상 클래스를 추상 메소드 이외에 일반 메소드들도 얼마든지 가질 수 있다.

 

추상 클래스나 메소드는 자바에서 abstract 키워드를 사용한다.

 

package me.xxxelppa.study.week06;
 
public class Exam_006 {
}
 
// 추상 메소드를 가지지 않는 추상 클래스
abstract class abstract_class_without_abstract_method { }
 
// 추상 메소드를 가지는 추상 클래스
abstract class abstract_class_with_abstract_method {
    abstract public void print();
}

추상 메소드 존재 여부와 무관하게 모든 추상 클래스는 인스턴스화 (객체화) 할 수 없다.

즉, new 키워드를 사용해서 객체를 생성할 수 없다.

 

그 이유는 의외로 단순하다.

구체적인 내용이 없기 때문에 이걸 구체적인 객체로 생성하지 못한다고 생각하면 된다.

 

실제로 11라인에서 작성한 abstract 키워드를 사용한 추상 메소드를 보자.

이 키워드가 붙은 메소드는 구현부를 가지고 있지 않다.

 

즉, 이 메소드를 실행 했을 때 뭘 할지 알 수 없다.

그럼 이 쓸모 없어 보이는 것은 왜 만들었을까?

 

사실 쓸모 없어 보이지만 쓸모가 있다. (이게 무슨 말장난)

이 객체도 만들 수 없는 추상 메소드를 가진 추상 클래스를 상속 받으면, 상속 받은 클래스에서는 이 추상 메소드를 무조건 구현 해주어야 한다.

 

즉, 다음과 같다.

 

package me.xxxelppa.study.week06;
 
public class Exam_006 extends abstract_class_with_abstract_method {
    @Override
    public void print() {
        System.out.println("무조건 구현 해주어야 합니다.");
    }
}
 
// 추상 메소드를 가지지 않는 추상 클래스
abstract class abstract_class_without_abstract_method { }
 
// 추상 메소드를 가지는 추상 클래스
abstract class abstract_class_with_abstract_method {
    abstract public void print();
}

 

부모 클래스를 작성 하면서 다음과 같이 생각할 수 있다.

'이 메소드는 상속 받은 클래스마다 다르게 구현 해야하는 기능인데, 구현을 강제할 방법이 없을까'

일반 메소드라면 자식 클래스에서 오버라이딩이 선택이기 때문에 강제할 수 없다.

하지만 추상 메소드를 작성 해둔다면 상속 받은 자식은 무조건 구현해야한다는 제약을 가지게 된다.

 

추상 메소드를 구현하지 않으면 컴파일 단계에서 오류를 발생하기 때문에, 런타임 오류를 방지할 수 있다는 장점도 가지고 있다.

만약 추상 메소드를 가지는 추상 클래스를 상속 받았는데 메소드를 구현하지 않으면 다음과 같은 오류 메시지를 받아볼 수 있다.

 

Class 'Exam_006' must either be declared abstract or implement abstract method 'print()' in 'abstract_class_wih_abstract_method'

 

추상 클래스가 자신을 상속받은 자식 클래스에서 자신이 가진 추상 메소드 구현을 강제 하는 것도 있지만,

추상 메소드를 가지지도 않으면서 추상 클래스로 선언하는 경우가 있다.

개인적으로 흔하게 보지 못했지만, 그 클래스의 객체를 생성해서 사용하면 안되는 경우 추상 클래스로 정의 하기도 한다.

 

 

 

final 키워드


이름이 주는 느낌이 어떤 느낌인지 알았다면, 그 느낌이 맞다.

이거 진짜 최종, 더 이상 진짜 수정 없음, 끝 이라고 하고 싶을 때 사용하는 키워드 이다.

 

클래스를 정리 하면서 등장 했지만, 사실 클래스 이외에 변수에도 사용할 수 있다.

변수에 사용할 경우 이 변수를 앞으로 상수 취급 한다는 것을 의미한다.

상수는 그 자체로 다른 값을 가질 수 없다.

다시 말해서 변수의 형태를 가지고 있지만, final 키워드를 붙였다면 이 변수는 다른 값을 가질 수 없음을 뜻한다.

 

그렇기 때문에 보통 final 키워드를 사용하면 값을 처음에 무조건 할당 해줘야 한다.

final 키워드가 붙은 변수는 다음과 같은 방법으로 값을 할당할 수 있다.

 

package me.xxxelppa.study.week06;
 
public class Exam_007 {
    private final int MY_FINAL_INT;
    private final String MY_FINAL_STRING = "FINAL_STRING";
    public Exam_007(int MY_FINAL_INT) {
        this.MY_FINAL_INT = MY_FINAL_INT;
    }
    
    public static void main(String[] args) {
        final double PI = 3.14;
    }
}

 

4라인에서 final 변수를 선언 했지만 값을 할당하지 않았다.

그래도 가능한 이유는 ​생성자를 통해 값을 할당​하고 있기 때문이다.

그 외의 경우에는 무조건 선언과 동시에 값을 할당 해주어야 한다.

 

추가로 final 변수에 대한 네이밍 컨벤션이 존재한다.

모두 대문자로 작성하고 언더 스코어(_)로 연결한 변수명을 사용해서 '누가 봐도 final 변수구나' 하고 알 수 있도록 한다.

 

 

다음으로 메소드에도 붙일 수 있는데, 이런 경우 오버라이드 할 수 없는 메소드가 된다.

변수가 상수가 된 것처럼 같은 이름으로 다른 기능을 하도록 재정의 할 수 없다는 것을 뜻한다.

 

마지막으로 클래스에 붙인 경우 상속할 수 없는 즉, 부모 클래스가 될 수 없음을 뜻한다.

하지만 final 클래스는 여전히 부모 클래스를 가질 수 있다.

 

 

 

Object 클래스


Object 클래스는 단군 할아버지 같은 존재다. 모든 클래스의 최상위 부모는 항상 이 Object 클래스이다.

이 말의 의미는 모든 클래스는 Object 클래스에 정의 되어있는 모든 멤버 필드와 메소드를 자유롭게 가져다 사용할 수 있음을 뜻한다.

 

Object 클래스에 대한 api 문서 링크

 

이 클래스에서 다른 어떤 메소드 보다 주의해야 할 것은 finalize() 메소드이다.

간단하게 말하면, 이 메소드는 GC를 호출하는 메소드인데 사실 직접 호출하면 여러가지 문제가 발생할 수 있다.

 

자세한 내용에 대해 잘 정리된 글을 링크로 남겨둔다.

 

그래서인지 api 문서 상에도 deprecated 가 되었다고 명시 되어 있다.

 

Deprecated.
The finalization mechanism is inherently problematic. Finalization can lead to performance issues, deadlocks, and hangs. Errors in finalizers can lead to resource leaks; there is no way to cancel finalization if it is no longer necessary; and no ordering is specified among calls to finalize methods of different objects. Furthermore, there are no guarantees regarding the timing of finalization. The finalize method might be called on a finalizable object only after an indefinite delay, if at all. Classes whose instances hold non-heap resources should provide a method to enable explicit release of those resources, and they should also implement AutoCloseable if appropriate. The Cleaner and PhantomReference provide more flexible and efficient ways to release resources when an object becomes unreachable.

 

 

 

 

728x90

'Archive > Java online live study S01' 카테고리의 다른 글

8주차 : 인터페이스  (0) 2021.05.01
7주차 : 패키지  (0) 2021.05.01
5주차 : 클래스  (0) 2021.05.01
4주차 : 제어문  (0) 2021.05.01
3주차 : 연산자  (0) 2021.05.01
728x90

# 자바의 Class에 대해 학습하세요.


# 학습할 것

  • 클래스 정의하는 방법
  • 객체 만드는 방법 (new 키워드 이해하기)
  • 메소드 정의하는 방법
  • 생성자 정의하는 방법
  • this 키워드 이해하기

 

클래스를 정의하는 방법


클래스를 정의하는 방법은 다음과 같이 자바에 class 라는 키워드를 사용하는 것이다.

package me.xxxelppa.study.week05;
 
public class Exam_001 {
    
}

위 예제는 Exam_001 이라는 이름의 클래스를 정의한 것을 보여준다.

 

클래스를 정의하는 것 자체는 별로 어렵지 않다고 생각한다.

문제는 잘 만드는 것이다.

 

자바 공부를 중간에 포기한 사람도 자바는 객체지향 언어라는 것은 들어봤을 것이다.

여기서 말하는 이 객체를 자바에서는 클래스를 통해 만들어 진다.

다시 말해서 객체를 만들기 위해 클래스를 정의한다고 생각하면 된다.

 

간혹 이것 때문에 객체와 클래스를 동일한 것으로 착각 하기도 한다.

그래서 보통의 기본서에서 이 둘의 관계를 붕어빵과 붕어빵 틀이라고 설명하고 있다.

붕어빵 틀이 클래스, 붕어빵이 객체.

틀린 말은 아니라고 생각하지만 처음엔 무슨 말인지 잘 이해하지 못했었다.

(심지어 객체가 뭐냐고 물어봤을 때 '붕어빵이요' 라는 대답을 들은 적도 있다.)

 

개인적으로 클래스는 실체를 상상할 수 있는 추상화된 설계서라고 하고 싶다.

예를 들어 '자동차' 설계 도면을 보면 바퀴가 있고 앞으로도 가고 뒤로도 가는 탈 것을 떠올릴 수 있다.

그리고 실제 도로에 보이는 저 자동차 하나하나는 '자동차' 라고 하는 추상적인 것을 실물로 만들어낸 객체이다.

(클래스를 통해 만들어진 객체가 어떤 속성을 가지며 어떤 동작을 할 수 있는지 설계해 놓은 것이 클래스이다.)

 

사람은 클래스이고 객체가 아니다.

하지만 철수는 사람이지만 객체이지 클래스가 아니다.

 

컴퓨터는 클래스이고 객체가 아니다.

하지만 지금 내 눈 앞에 존재하는 이 컴퓨터는 컴퓨터라고 부르지만 클래스가 아니고 객체이다.

 

그렇기 때문에 클래스는 그 자체로는 사용할 수 없다.

즉, 클래스를 통해 객체화 되어야 실제로 사용할 수 있다.

 

 

그렇기 때문에 클래스의 이름을 지을 때는 특히 조심해야 한다.

몇 가지 네이밍 컨벤션이 존재한다.

명사여야 한다던가 영문 대문자로 시작하도록 하는 것들이 그렇다. (메소드는 영문 소문자로 작성 하도록 권장한다.)

꼭 지켜야 프로그램이 동작하는 것은 아니지만, 이름을 보고 무엇인지 유추할 수 있도록 이름을 짓는 것이 여러가지로 정신 건강에 이롭다.

 

 

바로 이 클래스는 크게 두 가지를 담고 있다. 속성과 행위이다.

앞서 자동차 얘기를 했으니 자동차 클래스를 만든다고 생각 해보자.

자동차가 가질 수 있는 속성으로는 제조사, 색상, 연비, 시트 재질, 최대 탑승 입원, 종류 등이 있다.

행위로는 앞으로 가기, 뒤로 가기, 옆으로 가기, 방향 지시등 켜기, 에어컨 켜기, 에어컨 끄기 등이 있다.

 

정의하기 나름이지만 일반적으로 위와 같이 속성과 행위(또는 기능이라고 하기도 한다)를 가지고 있다.

일반적으로 속성을 다른 말로 멤버 필드 라고도 하며, 행위는 메소드 라고 부른다.

 

 

클래스에 대해 간단히 정리하면

1. 클래스 자체로는 아무것도 할 수 없다. (리플렉션 같은 특수한 경우 제외)

2. 객체화 해야 비로소 사용할 수 있다.

3. 오류가 발생하지 않지만, 네이밍 컨벤션을 되도록 지켜줘야 한다.

4. 클래스는 속성과 행위 내용을 담을 수 있다. (꼭 담아야 하는 것은 아니다.)

 

정도가 될 수 있을 것 같다.

 

마지막으로 하나의 java 파일 안에 다수의 클래스를 정의할 수 있다.

하지만 public 키워드가 붙은 class 는 단 하나만 존재해야 하며, 이 클래스는 파일 이름과 완벽하게 동일한 이름을 가져야 한다.

되도록 하나의 파일에는 하나의 클래스만 정의하는게 일반적이다. (중첩 클래스 제외)

 

** 클래스 내에 클래스를 선언하는 중첩 클래스도 있는데, 우선 일반적인 경우만 간략하게 정리 했다.

 

 

728x90

 

 

 

객체 만드는 방법 (new 키워드 이해하기)


자바에서 클래스에 대한 객체 뿐만 아니라 객체를 만들 때 new 라는 키워드를 사용한다.

이 키워드를 사용하면 이 객체를 동적 메모리 할당 영역에 생성한다.

이 영역에 할당한다는 것은 GC에 의해 관리된다는 것을 의미한다.

 

현재 클래스에 대해 정리하고 있으니 클래스의 객체를 생성하는 방법에 대해 알아보려 한다.

하지만 그 전에 클래스에 대한 개인적인 이해를 하나 더 정리해두려 한다.

 

이전에 이미 자료형에 대해 정리를 했었다.

결론부터 얘기하자면 클래스도 하나의 자료형 이다.

앞서 정리한 자료형과는 자바가 기본적으로 제공을 해주느냐 아니면 사용자(여기서 사용자는 코드를 작성하는 사람)가 정의한 자료형이냐의 차이가 있다.

 

자료형에 대한 기억은 잠시 되짚어보면, int 라는 기본 정수형 자료형을 보면 우리가 기본적으로 기대하는게 있다.

정수를 담을 수 있고, 그 값의 범위는 대략 -21억 ~ +21억이며, 다른 기본 자료형에 타입 캐스팅 되었을 때의 기대값 등 어떤 형태를 가지고 어떤 동작을 할지 상상할 수 있다.

즉, int 타입의 자료형으로 선언된 변수에 담긴 값이 어떤 속성과 어떤 행위들을 할 수 있는지 우리는 int 라는 것을 보고 알 수 있다.

 

클래스와 매우 유사하다고 느껴지지 않는다면 나의 잘못이다.

 

 

다른 얘기로 돌아왔지만, 객체를 생성하는 방법은 다음과 같다.

 

// 객체화 할 임의의 클래스
class TargetClass { }
 
// TargetClass 클래스의 객체를 생성 할 클래스
public class MainClass {
    // 굳이 main 메소드 안에서 생성 할 필요는 없다.
    TargetClass tc = new TargetClass();
    int myValue = 10;
}

 

7라인에서 TargetClass 클래스 타입의 변수 tc 를 선언하고, 그 안에 new TargetClass() 를 사용하여 생성한 객체를 담고 있다.

여기서 TargetClass tc 이 부분이 일반 자료형의 변수를 선언한 것과 매우 닮아있다는 것을 눈여겨 보았으면 좋겠다.

 

8라인의 int myValue 는 int 자료형 타입의 변수 myValue 를 선언한 것이지 아직 값이 담긴것이 아니다.

할당 연산자인 등호(=) 기준 오른쪽의 값이 int 타입의 자료형에 담길 수 있는 값이기 때문에 컴파일 오류가 발생하지 않고 값을 담고 있다.

이것과 마찬가지로 TargetClass 타입의 자료형 변수 tc를 선언 하였으며,

컴파일 오류가 발생하지 않는 이유는 할당 연산자인 등호(=) 기준 오른쪽 값이

new TargetClass() 의 결과가 TargetClass 타입의 값이 될 수 있기 때문이다.

 

 

객체 생성 방법이 대해 알아보다 갑자기 또 클래스 얘기를 해버렸지만,

하고 싶은 얘기는 클래스도 결국 사용자가 정의한 하나의 자료형이라는 것이다.

 

 

마지막으로 java에는 생략할 수 있는 것들이 생각보다 많이 있다.

위 예제에서 객체를 생성함에 있어서 생략된 부분을 명시하면 다음과 같다.

 

// 객체화 할 임의의 클래스
class TargetClass {
    public TargetClass() { }    // 생략된 부분
}
 
// TargetClass 클래스의 객체를 생성 할 클래스
public class MainClass {
    // 굳이 main 메소드 안에서 생성 할 필요는 없다.
    TargetClass tc = new TargetClass();
    int myValue = 10;
}

 

3라인에 작성한 부분이 ​사용자가 작성하지 않아도 이미 작성한 것 처럼​ 동작하는 생략된 코드이다.

기본 생성자 라고 부르며 자신이 속한 클래스 이름과 완벽히 똑같은 이름을 가지며 매개변수가 아무것도 없는 것이 특징이다.

 

여기서 눈여겨 볼 것은 9라인에서 new TargetClass() 문장에서 ​매개변수가 없다는 것​과 3라인의 기본 생성자의 ​매개변수 또한 없다는 것​이다.

객체를 생성하려면 해당 클래스의 생성자와 매개변수를 일치시켜야 한다. (매개변수의 타입까지)

 

만약 그렇지 않다면 객체는 생성되지 않는다.

 

예를 들면 다음과 같이 작성하면 TargetClass의 객체는 생성할 수 없다.

 

// 객체화 할 임의의 클래스
class TargetClass {
    // private 접근 제한자로 외부에서 생성자에 접근할 수 없도록 제한한다.
    private TargetClass() { }
}
 
// TargetClass 클래스의 객체를 생성 할 클래스
public class MainClass {
    // 굳이 main 메소드 안에서 생성 할 필요는 없다.
    // 동일한 매개변수를 받는 생성자에 접근할 수 없으므로 컴파일 오류가 발생한다.
    TargetClass tc = new TargetClass();
    int myValue = 10;
}

 

실제로 작성 해보면 'TargetClass()' has private access 라는 메시지를 보여줄 것이다.

여기서 TargetClass 클래스가 아니고 TargetClass() 이 생성자가 private 접근만 허용하기 때문이다.

 

생성자에 대한 자세한 내용은 잠시 뒤에 정리 해보려 한다.

 

 

 

메소드 정의하는 방법


메소드는 클래스 내부에 정의할 수 있으며, 클래스와 마찬가지고 네이밍 컨벤션을 가지고 있다.

일반적으로 동사 형태를 사용하고, 영문 소문자로 시작하도록 권장한다.

 

C언어를 먼저 공부한 사람들이 보통 메소드와 함수라는 용어를 혼용해서 사용하는데, 메소드는 독립적으로 존재할 수 없기 때문에 되도록 구분해야 한다고 생각한다.

 

 

자바에는 생략 가능한 키워드들이 많다.

예를 들면 import 가 그렇다.

 

갑자기 다른 소리를 하게 되었는데, 자바에는 굉장히 많은 클래스가 존재한다.

그렇기 때문에 중복되는 경우도 많다.

쉽게 설명하면 동명이인이라고 생각하면 될 것 같다.

 

그럼 어쨌든 동명이인을 구분 할 필요가 있는데, 자바에서는 패키지라는 것으로 구분한다.

패키지는 폴더라고 생각하면 된다.

같은 폴더 내에는 같은 이름의 파일을 저장할 수 없다. (확장자가 다른 경우는 생각하지 않는다.)

같은 이름의 파일을 저장하는 방법은, 이름을 바꾸는 방법도 있지만 (예를 들어 버전을 명시 한다던가)

폴더를 하나 더 만들어서 저장하면 된다.

 

아무튼

자바를 처음 시작할 때 Hello World 를 출력한 기억이 있을 것이다.

그 때 아무런 의심 없이 다음과 같은 코드를 작성 했을 것이다.

 

public class MyFirstJava {
    public static void main(String[] ar) {
        System.out.println("Hello World");
    }
}

 

다른 것들도 그렇지만 System.out.println 이라는 것은 어디서 왔을까?

생긴걸 보면 소괄호를 사용하고, 문자열을 전달하는 것을 봐서 메소드 같긴 한데...

나는 그 어디에도 System 이나 out 또는 println 에 대해 그 무엇도 하지 않았는데

컴파일도 잘 되고 심지어 실행도 잘 된다.

 

자바 api 문서에서 찾아보면 System 이라는 것은 (영문 대문자로 시작했기 때문에 클래스 라고 유추할 수 있다.)

lang 이라는 패키지 안에 정의 되어 있다고 나온다.

 

 

즉, 다시 말해서 우리는 lang 이라는 패키지(폴더)안에 있는 System 이라는 클래스를 가져다 사용한 것이다.

그런데 작성한 코드 어디를 봐도 관련된 내용을 찾아볼 수 없다.

자바에서는 따로 작성하지 않아도 import java.lang.*; 라는 문장을 있는 것처럼 취급한다.

그래서 따로 정의하지 않고도 사용할 수 있었던 것이다.

 

 

생략 가능한 키워드에 대해 얘기 하다 주제에서 많이 벗어났는데, 메소드 정의에도 생략할 수 있는 것이 있다.

우선 가장 기본적인 형태의 메소드는 다음과 같이 정의 할 수 있다.

 

[접근 제한자] [반환 자료형] [메소드 이름] ( [매개변수] ) {

    // 실행 블록

}

 

이것들 외에도 메소드 정의시 작성할 수 있는 것들이 더 많지만, 기본적인 것들에 대해서만 우선 정리해본다.

 

[1. 접근제한자]

  말 그래도 접근을 제한하는 키워드이다.

그 종류로는 public, protected, private 등이 있다.

간혹 default 또는 package 라고 하는 같은 패키지 내에서만 접근이 가능한 접근 제한자가 있다고 하지만, 키워드가 따로 존재하지 않고, 아무것도 작성하지 않으면 이 접근 레벨을 갖는다.

 

public 은 모든 접근을 허용하고, protected 는 같은 패키지에 속하거나 상속 관계에서 접근할 수 있도록 한다.

마지막으로 private은 선언된 클래스 내에서만 접근이 가능하다.

 

 

여기서 한 가지 설명할 것이 있다. 바로 객체지향의 특징중에 캡슐화와 은닉화이다.

캡슐화와 은닉화를 같은 것으로 취급하는 것을 많이 봤지만, 개인적으로 이것을 구분해서 이해했으면 하는 바람이다.

캡슐화 (encapsulation) 자체가 소중히 보호 한다는 의미도 있지만, 일반적으로 관련이 깊은 속성과 기능을 하나의 클래스로 묶는다는 의미가 강하다.

예를 들어 자동차 클래스를 정의 하는데, 그 안에 '보조석에서 과자를 먹는다' 같은 기능을 넣지 않는 것 처럼

자동차 클래스는 자동차 자체에 대해서만 깊은 연관이 있어야 한다.

 

은닉화(information hiding)는 속성을 외부에 노출시키지 않음을 의미한다.

예를 들어 자동차 색상을 red 에서 green 으로 바꾸고 싶다고 하자.

 

class Car {
    public String color = "red";
    
    public setColor(String newColor) {
        System.out.println("자동차 색상을 변경합니다.");
        System.out.println("비용이 입금 되었는지 확인 합니다.");
        System.out.println("페인트를 준비 합니다.");
        color = newColor;
        System.out.println("색상 변경이 완료 되었습니다.);
    }
}

 

현실과 많이 다르겠지만, 색상을 바꾸기 전과 후에 부가적인 작업이 필요하다고 하자.

이 작업들을 온전히 수행하려면 ​자동차 클래스를 설계한 사람의 의도대로​ 동작할 수 있도록 setColor() 메소드를 통해서만 변경 작업이 이루어 져야 한다.

즉, ​속성에 직접 접근해서 색상을 바꿔버리면​ 자동차 클래스를 설계한 사람의 의도와 다른 결과가 발생할 수 있다.

 

이러한 문제를 미리 방지하기 위해 ​일반적으로 속성은 private 으로, 메소드는 public 으로 선언​한다.

 

 

[2. 반환 자료형]

  이곳에는 사용자 정의 자료형을 포함하여 기본자료형, 인터페이스 등 모든 종류의 자료형을 기입할 수 있다.

그리고 사용자는 이 메소드를 호출해서 실행한 결과 값이 반환 자료형의 타입의 값임을 알 수 있다.

즉, 메소드 안에서 무슨 일이 일어나는지 모르겠지만 (또 상관도 없고) 내가 그 메소드 호출하면 그 타입의 값이나 주쇼! 라고 생각하면 된다.

 

 

[3. 메소드 이름]

  메소드 이름에 대해서는 본 주제의 맨 처음에도 언급 헀지만, 네이밍 컨벤션이 있으므로 되도록 권고를 따라 작성하면 된다.

변수 이름을 포함하여 클래스, 메소드의 이름을 잘 짓는것은 가독성을 포함하여 유지보수 할 때 매우 중요하니 모호하지 않도록 잘 작성하는 것이 중요하다.

 

 

[4. 매개변수]

  해당 메소드를 호출할 때 필요한 값들을 전달 받을 수 있다. 하지만 그렇다고 꼭 사용해야 하는 것은 아니지만, 되도록이면 필요한 값들만 전달 받을 수 있도록 하는것이 좋다.

 

 

위에 정리한 내용을 바탕으로, 두 개의 정수를 입력 받아 합을 반환하는 메소드를 다음과 같이 정의할 수 있다.

 

public int add(int n1, int n2) {
    return n1 + n2;
}

 

메소드는 알겠는데 이 코드에 처음보는 키워드가 있다.

return 이라는 키워드 이다.

이 return 이라는 키워드는 메소드의 실행 결과를 반환하는 기능을 한다.

 

그렇기 때문에 이 return 키워드 우측에는 '값'이 와야 하며, 이 값은 반환 자료형 타입의 값이어야 한다.

이 키워드를 만나면 언제든 이 메소드를 종료 해버린다.

 

 

 

생성자 정의하는 방법


생성자를 정의 하는 방법은 간단하다.

클래스 이름과 완전히 동일한 메소드를 선언하면 되는데, 일반 메소드와 다른 점은 반환 자료형을 명시하지 않는 다는 것이다.

 

class MyClass {
    // 기본 생성자 :: 작성하지 않아도 있는 것으로 간주
    public MyClass() { }
}

 

이 생성자는 여러개를 중복해서 선언할 수 있다.

대신 매개변수의 타입이나 개수가 달라 서로 식별이 가능해야 한다.

이것을 생성자 오버로딩이라고 한다.

 

class MyClass {
    // 기본 생성자 :: 작성하지 않아도 있는 것으로 간주
    public MyClass() { }
 
    public MyClass(int number) { }
 
    public MyClass(String str) { }
 
    public MyClass(int number, String str) { }
}

 

일반 메소드에서 그랬듯 매개변수로 받지만 굳이 사용하지 않아도 오류가 발생하지는 않는다.

하지만 일반적으로 생성자는 객체를 생성할 때 사용하기 때문에 보통 속성 값을 할당 (클래스 멤버 필드 라고도 한다) 할 값을 전달 받을 때 사용한다.

일반적인 사용 방법은 다음과 같다.

 

class MyClass {
    private int age;
    private String name;
 
    public MyClass() { }
 
    public MyClass(int age) {
        this.age = age;
    }
 
    public MyClass(String name) {
        this.name = name;
    }
 
    public MyClass(int age, String name) {
        this.age = age;
        this.name = name;
    }
}
 
class MainClass {
    public static void main(String[] ar) {
        MyClass myClass_1 = new MyClass();          // 기본 생성자로 객체 생성
        MyClass myClass_2 = new MyClass(10);        // 정수 타입 자료형 매개변수가 1개인 생성자로 객체 생성
        MyClass myClass_3 = new MyClass("이름");
        MyClass myClass_4 = new MyClass(20, "닉네임");
    }
}

 

앞에서 잠깐 언급하고 지나갔지만, 클래스의 속성을 멤버 필드 라고도 한다.

멤버 필드를 메소드 내에 정의한 지역 변수와 혼용해서 사용하기도 하는데, 구분해서 사용하는게 맞다.

메소드 내에 정의된 지역 변수는 값을 할당하지 않고 사용할 수 없는 반면

클래스 영역의 멤버 필드는 값을 할당하지 않아도 해당 데이터 타입의 기본 값으로 값이 설정 된다.

 

마지막으로 처음보는 this 라는 키워드가 있는데, 바로 다음에 살펴 보도록 하자.

 

 

 

 

this 키워드 이해하기


this 를 보면 일단 이 키워드를 감싸고 있는 가장 가까운 클래스를 나타낸다고 생각하면 된다.

javascript 를 먼저 공부했다면 this 가 나타내는 값이 꽤나 단순해졌다고 느낄 수 있다.

 

어쨌든 this는 클래스 입장에서 '나' 라고 말하는 것과 같다.

 

바로 위 생성자를 정의하는 방법에서 작성한 코드에서 MyClass 클래스를 다시 한 번 보자.

 

class MyClass {
    private int age;
    private String name;
 
    public MyClass() { }
 
    public MyClass(int age) {
        this.age = age;
    }
 
    public MyClass(String name) {
        this.name = name;
    }
 
    public MyClass(int age, String name) {
        this.age = age;
        this.name = name;
    }
}

 

this 키워드를 꼭 생성자에서만 사용하는것은 아니지만, 생성자에서 흔히 볼 수 있다.

7라인의 정수형 타입 매개 변수 age 가 있다.

이제부터 MyClass(int age) { } 블록 내에서 이 값은 age 라는 변수를 사용하여 그 안에 담긴 값을 사용할 수 있다.

그런데 문제가 생겼다.

MyClass의 멤버 필드 영역에 속성으로 같은 타입의 변수 age 가 이미 선언되어 있기 때문이다.

 

즉, this 가 없다면 8 라인은 age = age 라고 쓰게 될 것이고, 이것은 굉장히 모호한 코드가 된다.

매개 변수 age 에 멤버 필드 age 값을 할당 한다는 것인지.

멤버 필드 age 에 매개 변수 age 값을 할당 한다는 것인지.

아니면 아무런 의미도 없이

멤버 필드 age 에 멤버 필드 age 값을 할당 한다는 것인지.

매개 변수 age 에 매개 변수 age 값을 할당 한다는 것인지.

 

사람도 혼란스러운데 컴퓨터가 구분할 수 있을리 없다.

 

이 문제를 this 키워드를 사용해서 해결할 수 있다.

(8라인에서 age = age 라고 할 경우, 둘 다 자신의 스코프에서 가장 가까운 매개변수 age를 가리킨다.)

즉, this.age는 매개 변수의 age가 아닌, 클래스 자기 자신에 선언 된 멤버 필드 age를 뜻한다는 것을 ​명시적으로​ 나타낸 것이다.

 

만약 메소드의 매개 변수의 이름과 동일한 이름의 멤버 필드가 없다면, 각자 어떤 변수를 나타내는지 모호하지 않기 때문에 굳이 this를 사용하지 않아도 된다.

 

 

다음으로 this() 메소드이다.

this() 메소드는 생성자 내에서만 쓰일 수 있으며, 자신이 속한 클래스의 생성자를 가리킨다.

심지어 작성 위치도 생성자의 최상단으로 정해져 있다. 그 사이에 어떠한 코드도 와서는 안된다.

예를 들면 다음과 같이 쓸 수 있다.

 

public class MyClass {
    public MyClass() {
        System.out.println("기본 생성자 입니다.");
    }
 
    public MyClass(int number) {
        this();
    }    
}

 

6라인과 7라인 사이에 작성할 수 있는데 딱 하나 있는데, 주석이다.

그 외 어떠한 코드도 생성자 정의와 this() 사이에 올 수 없다.

 

그럼 이 this()는 언제 사용할까?

생성자를 여러개 정의했을 때 유용하게 사용할 수 있다.

 

다음 클래스를 보자.

 

public class MyClass {
    private String str_1;
    private String str_2;
    private String str_3;
 
    private int int_1;
    private int int_2;
    private int int_3;
 
    // 기본 생성자로, 기본값으로 할당
    public MyClass() {
        this.str_1 = "str_1 기본값";
        this.str_2 = "str_2 기본값";
        this.str_3 = "str_3 기본값";
 
        this.int_1 = -1;
        this.int_2 = -1;
        this.int_3 = -1;
    }
 
    public MyClass(String str_1, int int_1) {
        this.str_1 = str_1;        // 생성자 매개변수 값으로 입력
        this.str_2 = "str_2 기본값";
        this.str_3 = "str_3 기본값";
 
        this.int_1 = int_1;        // 생성자 매개변수 값으로 입력
        this.int_2 = -1;
        this.int_3 = -1;
    }
 
    public MyClass(String str_1, int int_1, String str_2, int int_2) {
        this.str_1 = str_1;                // 생성자 매개변수 값으로 입력
        this.str_2 = str_2;                // 생성자 매개변수 값으로 입력
        this.str_3 = "str_3 기본값";
 
        this.int_1 = int_1;        // 생성자 매개변수 값으로 입력
        this.int_2 = int_2;        // 생성자 매개변수 값으로 입력
        this.int_3 = -1;
    }
}

 

위와 같은 경우가 얼마나 많을지는 모르겠지만, 이렇게 중복되는 코드가 많은 경우 다음과 같이 this() 메소드를 사용해서 작성할 수 있다.

 

public class MyClass {
    private String str_1;
    private String str_2;
    private String str_3;
 
    private int int_1;
    private int int_2;
    private int int_3;
 
    // 기본 생성자로, 기본값으로 할당
    public MyClass() {
        this.str_1 = "str_1 기본값";
        this.str_2 = "str_2 기본값";
        this.str_3 = "str_3 기본값";
 
        this.int_1 = -1;
        this.int_2 = -1;
        this.int_3 = -1;
    }
 
    public MyClass(String str_1, int int_1) {
        this();                    // 기본 생성자 메소드 호출
        this.str_1 = str_1;        // 생성자 매개변수 값으로 입력
        this.int_1 = int_1;        // 생성자 매개변수 값으로 입력
    }
 
    public MyClass(String str_1, int int_1, String str_2, int int_2) {
        this(str_1, int_1);        // 매개변수가 일치하는 생성자 호출
        this.str_2 = str_2;        // 생성자 매개변수 값으로 입력
        this.int_2 = int_2;        // 생성자 매개변수 값으로 입력
     }
}

 

28라인처럼 매개변수를 입력하면, 타입과 개수가 일치하는 생성자를 호출한다.

그래서 28라인의 코드는 21라인의 생성자를 호출하고, 그 안에서 다시 11라인의 기본 생성자를 호출하게 된다.

 

 

마지막으로 정리 할 내용에는 없지만, this와 생성자 관련해서 키워드가 하나 더 있다.

super와 super() 인데, this와 달리 super는 자신의 부모 클래스와 부모 클래스의 생성자를 호출할 때 사용할 수 있다.

 

 

 

 

728x90

'Archive > Java online live study S01' 카테고리의 다른 글

7주차 : 패키지  (0) 2021.05.01
6주차 : 상속  (0) 2021.05.01
4주차 : 제어문  (0) 2021.05.01
3주차 : 연산자  (0) 2021.05.01
2주차 : 자바 데이터 타입, 변수 그리고 배열  (0) 2021.05.01
728x90

 

# 자바가 제공하는 제어문을 학습하세요.


# 학습할 것

  • 선택문
  • 반복문

 

선택문


실행 코드를 제어하는 방법 중 선택문에 대해 정리해보려 한다.

또 다른 말로 조건문 이라고 알고 있는 이 선택문은 크게 if 와 switch 가 있다.

지난주에 switch에 대해 알아보았으니, 이번엔 if에 대해 정리해보려 한다.

 

구분하기 나름이겠지만 if 문은 크게 세가지 형태가 있다.

1. if (조건) { 실행 블록 }

2. if (조건) { 조건이 참일 때 실행 블록 } else { 조건이 거짓일 때 실행 블록 }

3. if (조건1) { 조건1이 참일 때 실행 블록 } else if (조건2) { 조건2가 참일 때 실행 블록 }

 

말로 보면 혼란스러우니 코드를 보자.

 

package me.xxxelppa.study.week04;
 
public class Exam_001 {
    public static void main(String[] args) {
        boolean condition_1 = true;
        
        if(condition_1) {
            System.out.println("condition_1 이 참 입니다");
        }
    
        if(condition_1) System.out.println("condition_1 이 참 입니다");
        
    }
}

 

condition_1 이 참 입니다
condition_1 이 참 입니다

 

실행 블록 내의 코드가 한 줄인 경우 중괄호를 생략할 수 있다.

 

if 조건문의 소괄호 안의 내용은 boolean 으로 (참/거짓) 판별할 수 있는 값이 들어와야 한다.

 

package me.xxxelppa.study.week04;
 
public class Exam_002 {
    public static void main(String[] args) {
        boolean condition_1 = true;
        boolean condition_2 = false;
        
        if(condition_1 && condition_2) {
            System.out.println("condition_1과 condition_2 모두 참 입니다");
        } else {
            System.out.println("condition_1과 condition_2 중에 거짓이 있습니다.");
        }
    
        if(condition_1 && condition_2) System.out.println("condition_1과 condition_2 모두 참 입니다");
        else System.out.println("condition_1과 condition_2 중에 거짓이 있습니다.");
        
        /*
         * 심지어 이렇게 작성해도 잘 동작 하지만
         * 권장하지 않는 방법.
         */
        if(condition_1 && condition_2) {
            System.out.println("condition_1과 condition_2 모두 참 입니다");
        } else System.out.println("condition_1과 condition_2 중에 거짓이 있습니다.");
        
    }
}

 

condition_1과 condition_2 중에 거짓이 있습니다.
condition_1과 condition_2 중에 거짓이 있습니다.
condition_1과 condition_2 중에 거짓이 있습니다.

 

이번엔 두 번째 스타일의 코드를 작성 해보았다.

소괄호 안에 작성한 논리 결과가 참인 경우와 거짓인 경우에 따라 어떤 실행 블록을 처리할지 달라진다.

 

이번에는 조건이 다양한 경우에 대한 예시이다.

 

package me.xxxelppa.study.week04;
 
public class Exam_003 {
    public static void main(String[] args) {
        int score = 87;
    
        if (score >= 90) {
            System.out.println("매우 우수합니다.");
        } else if (score >= 80) {
            System.out.println("준수합니다.");
        } else if (score >= 70) {
            System.out.println("노력이 필요합니다.");
        } else if (score >= 60) {
            System.out.println("많은 노력이 필요합니다.");
        } else {
            System.out.println("뭔가 잘못 되었습니다.");
        }
        
        if (score >= 90) System.out.println("매우 우수합니다.");
        else if (score >= 80) System.out.println("준수합니다.");
        else if (score >= 70) System.out.println("노력이 필요합니다.");
        else if (score >= 60) System.out.println("많은 노력이 필요합니다.");
        else System.out.println("뭔가 잘못 되었습니다.");
        
    }
}

 

준수합니다.
준수합니다.

 

if 문의 특징은 한 번이라도 조건에 만족하는 경우를 찾으면, 그 다음 조건에 대해서는 생략한다는 것이다.

위의 예시에서 보면 score 값이 87 의 값을 가지기 때문에 60과 같거나 크고, 70과 같거나 크고 또 80과 같거나 크기 때문에

혹시 세 개의 실행 블록을 처리한다고 생각할 수 있지만

가장 먼저 만족하는 조건에 대한 실행 블록만 실행하기 때문에

때에 따라 조건을 확인하는 순서에 유의해서 작성해야 한다.

 

서로 연관이 없는 경우 else if 로 연결하여 하나의 선택문을 만들지 않고

새로운 if 문장을 실행하여 독립적으로 검사하는 문장을 작성할 수 있다.

 

 

이런 경우는 드물겠지만, 굳이 예를 든다면 다음과 같은 예를 들 수 있을것 같다.

 

package me.xxxelppa.study.week04;
 
public class Exam_004 {
    public static void main(String[] args) {
        int point;
        int score = 87;
        
        /*
         * 검수에 따라 point 를 달리 지급하는 예시
         */
        
        // example 1
        point = 0;
        if (score >= 90) ++point;
        else if (score >= 80) ++point;
        else if (score >= 70) ++point;
        else if (score >= 60) ++point;
        
        System.out.println("example 1 : " + point);
        System.out.println();
        
        
        // example 2
        point = 0;
        if (score >= 90) ++point;
        if (score >= 80) ++point;
        if (score >= 70) ++point;
        if (score >= 60) ++point;
        
        System.out.println("example 2 : " + point);
    }
}

 

example 1 : 1

example 2 : 3

 

마지막으로 if 문 안에 또 다른 if 문을 얼마든지 중복 해서 사용할 수 있다.

하지만 너무 중복 해서 사용하면 가독성이 심하게 떨어질 수 있기 때문에 조심해야 한다.

그리고 실행 블록 { } 을 생략할 수 있다고 하지만 되도록 실행 블록을 작성 하는 것이 일반적으로 가독성이 더 좋다.

 

 

728x90

 

 

 

반복문


반복문은 어떤 조건이 만족하는 동안 같은 내용을 계속해서 반복하는 문장이다.

종류는 크게 세 가지가 있다.

1. for

2. while

3. do while

 

세가지 중에서 가장 많이 사용하는 것은 for문이다.

왜냐하면 while 문은 잘못하면 실행 블록이 무한정 반복할 수도 있기 때문이다.

 

가장 기본적인 for 문은 다음과 같다.

 

package me.xxxelppa.study.week04;
 
public class Exam_005 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; ++i) {
            System.out.println(i + " 번째 실행");
        }
    }
}

 

0 번째 실행
1 번째 실행
2 번째 실행
3 번째 실행
4 번째 실행
5 번째 실행
6 번째 실행
7 번째 실행
8 번째 실행
9 번째 실행

 

for문은 다음과 같은 특징을 갖는다.

1. 초기화식과 조건부, 증감부는 세미콜론 ; 으로 구분한다.

2. 초기화식은 for문이 실행될 때 단 한번만 실행 한다.

3. 조건부가 거짓이면 for문을 더이상 진행하지 않고 종료한다.

4. 조건부가 참이면 실행 블록을 실행하고 증감부를 실행한 다음 다시 한 번 조건부를 실행한다.

 

이렇게 계속 조건을 확인하면서 실행 블록을 반복 처리하는 것이 for 문이다.

 

 

만약 반복문을 사용하지 않고 동일한 결과를 얻기 위해서는 다음과 같이 작성할 수 있다.

 

package me.xxxelppa.study.week04;
 
public class Exam_006 {
    public static void main(String[] args) {
        System.out.println("0 번째 실행");
        System.out.println("1 번째 실행");
        System.out.println("2 번째 실행");
        System.out.println("3 번째 실행");
        System.out.println("4 번째 실행");
        System.out.println("5 번째 실행");
        System.out.println("6 번째 실행");
        System.out.println("7 번째 실행");
        System.out.println("8 번째 실행");
        System.out.println("9 번째 실행");
    }
}

지금은 10번 이지만, 100번 1,000번 반복 해야 한다고 하면 끔직한 일이 아닐 수 없다.

 

처음 for문을 배우면 간혹 틀에 갇혀 암기하는 경우를 많이 보았다.

 

package me.xxxelppa.study.week04;
 
public class Exam_007 {
    public static void main(String[] args) {
        for (int i = 0; i < args.length; ++i) {
            
        }
    }
}

 

for문을 배울 때면 배열에 대해 배운 다음이거나 같이 배우는 경우가 많다.

배열은 1부터 시작하지 않고 0부터 시작한다. (이를 zero base 라고도 한다.)

그래서 '포 인트 아이는 영 에서 [배열]의 길이보다 작은 동안 ++아이' 이렇게 통째로 암기하는 것을 본 적이 있다. (실화다)

 

for문을 다양하게 활용하기 위해서는 절대 잊지 말아야 하는 것이 있다.

1. 초기화식, 조건문, 증감문을 반드시 작성할 필요는 없다.

2. 초기화식, 조건문, 증감문을 얼마든지 확장해서 구현할 수 있다.

3. 증감문에 반드시 ++/-- (전위 또는 후위 증감 연산자) 같은 연산자를 사용할 필요는 없다.

4. 조건문에 사용하는 변수가 있는 경우, 이 변수가 꼭 초기화식에서 선언한 변수일 필요는 없다.

 

이것만 기억하고 잘 활용 한다면 다양하고 재미있는? 반복문을 만들 수 있다.

 

package me.xxxelppa.study.week04;
 
public class Exam_008 {
    public static void main(String[] args) {
        
        /*
         * 10부터 100까지 출력하는 예시
         *
         * 1. 반복문에서 사용 할 index 를 for 문 초기화식 밖에서 선언
         * 2. index 값을 1씩 증가하지 않고 10씩 증가
         */
        int index;
        for (index = 10; index <= 100; index += 10) {
            System.out.println("index : " + index);
        }
        // index 를 for문 밖에 선언 했기 때문에 for 문 종료 이후 참고하여 사용할 수 있다.
        System.out.println("최종 index : " + index);
        
    }
}

 

index : 10
index : 20
index : 30
index : 40
index : 50
index : 60
index : 70
index : 80
index : 90
index : 100
최종 index : 110

 

최종 index가 100이 아닌 110이 나온 이유는 굳이 설명하지 않아도 for문의 실행 순서를 기억한다면 알 수 있다.

 

또는 다음과 같은 것도 가능하다.

 

package me.xxxelppa.study.week04;
 
public class Exam_009 {
    public static void main(String[] args) {
        for(int i = 0, j = 10; i != j; ++i, --j) {
            System.out.println(i + " :: "+ j);
        }
    }
}

 

0 :: 10
1 :: 9
2 :: 8
3 :: 7
4 :: 6

 

초기화식에 하나가 아닌 두 개의 변수를 선언했고, 증감부에서 각 변수에 대한 조작을 넣었다.

증감부를 생략 할 수 있기 때문에 다음과 같이 작성해도 동일한 결과를 받아볼 수 있다.

 

package me.xxxelppa.study.week04;
 
public class Exam_010 {
    public static void main(String[] args) {
        for(int i = 0, j = 10; i != j; ) {
            System.out.println(i + " :: "+ j);
            ++i;
            --j;
        }
    }
}

 

초기화식 부분도 생략할 수 있기 때문에 다음과 같이 작성해볼 수도 있다.

 

package me.xxxelppa.study.week04;
 
public class Exam_011 {
    public static void main(String[] args) {
        int i = 0, j = 10;
        for(; i != j;) {
            System.out.println(i + " :: "+ j);
            ++i;
            --j;
        }
    }
}

생략이 가능하지만, 주의할 점은 이를 구분하는 세미콜론은 생략하면 안된다는 것이다.

반복문을 활용하는 방법이 무궁무진하기 때문에 사용하기 나름이라고 밖에 할 수 없을 것 같다.

 

 

** 심지어 다음과 같이 작성해도 잘 동작 한다.

 

package me.xxxelppa.study.week04;
 
public class Exam_021 {
    public static void main(String[] args) {
        System.out.println("================================= 피보나치 수열 =================================");
        for(int cnt = 0, bf = 0, af = 1; cnt++ < 30; System.out.print(cnt == 1 ? "1\t" : (af += bf) + (cnt % 10 == 0 ? "\n" : "\t")), bf = cnt == 1 ? bf : af - bf);
    }
}

 

더 다양하고 비범하게 사용할 수 있겠지만 과도한 응용은 정신 건강에 해로울 수 있으니 적절하게..

 

 

 

for문에 대한 마지막으로, '향상된 for문' (또는 개선된 for문) 이라고 불리는 문법이 있다.

반복문과 배열을 같이 사용하는 경우가 많기 때문에, 배열을 예시로 들어보자.

 

만약 반복 처리의 대상이 배열의 모든 요소라면 다음과 같이 작성해볼 수 있다.

 

package me.xxxelppa.study.week04;
 
public class Exam_012 {
    public static void main(String[] args) {
        int[] myArray = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int sum = 0;
        
        /*
         * for 문 역시 실행 블록이 한 줄이라면 블록 { }을 생략할 수 있다.
         */
        for (int i = 0; i < myArray.length; ++i) sum += myArray[i];
    
        System.out.println("총합 : " + sum);
    }
}

 

이것을 향상된 for문으로 고치면 다음과 같이 할 수 있다.

 

package me.xxxelppa.study.week04;
 
public class Exam_013 {
    public static void main(String[] args) {
        int[] myArray = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int sum = 0;
        
        /*
         * for 문 역시 실행 블록이 한 줄이라면 블록 { }을 생략할 수 있다.
         */
        for (int elem : myArray) sum += elem;
        
        System.out.println("총합 : " + sum);
    }
}

 

이 부분에 대해 호기심이 생겨 바이트 코드를 열어보기로 했다.

 

 

너무 작아서 잘 안보이겠지만

색칠 된 부분이 서로 다른 부분이다.

 

바이트코드의 op코드가 향상된 for 문으로 작성했을 때 조금 더 많아 성능 차이가 있을 수도 있겠다고 생각했다.

그래서 호기심이 생겨 여러가지 테스트를 해보았는데, 성능 차이를 느낄 수 없었다.

 

 

마지막으로 for 문에 대한 작은 팁? 이라면 팁이고 주의해야 한다면 주의해야 할 부분이 하나 있다.

그것은 조건부 영역에서 배열이나 collection 계열의 자료구조를 사용할 때 크기를 가지고 판단하는 경우가 많다.

 

package me.xxxelppa.study.week04;
 
import java.util.ArrayList;
import java.util.List;
 
public class Exam_014 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        int sum;
        
        /*
         * 테스트하기 위한 선행 작업
         * 픽스처 (fixture) 라고도 한다.
         */
        for(int i = 0; i < 100; ++i) list.add(i);
        
        /*
         * size 를 사용했을 때와 사용하지 않았을 때 실행 시간을 비교해 본다.
         */
        
        // 1. 조건부에 size 를 사용했을 경우
        long case_1_start_time = System.nanoTime();
        sum = 0;
        for (int i = 0; i < list.size(); ++i) {
            sum += list.get(i);
        }
        long case_1_end_time = System.nanoTime();
        System.out.println("case 1 :: " + (case_1_end_time - case_1_start_time));
        
        // 2. 조건부에 미리 구해둔 size 를 사용했을 경우
        long case_2_start_time = System.nanoTime();
        sum = 0;
        int size = list.size();
        for (int i = 0; i < size; ++i) {
            sum += list.get(i);
        }
        long case_2_end_time = System.nanoTime();
        System.out.println("case 2 :: " + (case_2_end_time - case_2_start_time));
    }
}

 

case 1 :: 27300
case 2 :: 16000

 

case 1 은 조건부에서 list 의 size() 를 확인해서 더 반복할지 판단하도록 구현했고

case 2 는 조건부에서 미리 구한 size 를 확인해서 더 반복할지 판단하도록 구현했다.

 

list 의 크기가 100정도 뿐인데 (단위가 ns 이지만) 차이를 보이고 있다.

어떻게 보면 작은 차이지만 크기가 커지면 성능에 무시할 수 없는 영향을 줄 수 있다.

 

하지만 때에 따라서 반복문 내에서 list 의 크기를 조작하여

매번 최신의 크기를 확인해야 할 경우도 있을 수 있으니 상황에 맞게 의도한 대로 동작 하도록 잘 구현 해야 한다.

 

 

다음으로 while 반복문의 기본적인 생김새를 알아보자.

 

package me.xxxelppa.study.week04;
 
public class Exam_015 {
    public static void main(String[] args) {
        int loopCnt = 10;
        int execCnt = 0;
        
        while(execCnt < loopCnt) {
            System.out.println("현재 " + ++execCnt + "번째 반복 중입니다.");
        }
        System.out.println();
        
        execCnt = 0;
        while(execCnt < loopCnt) System.out.println("현재 " + ++execCnt + "번째 반복 중입니다.");
    }
}

 

현재 1번째 반복 중입니다.
현재 2번째 반복 중입니다.
현재 3번째 반복 중입니다.
현재 4번째 반복 중입니다.
현재 5번째 반복 중입니다.
현재 6번째 반복 중입니다.
현재 7번째 반복 중입니다.
현재 8번째 반복 중입니다.
현재 9번째 반복 중입니다.
현재 10번째 반복 중입니다.

현재 1번째 반복 중입니다.
현재 2번째 반복 중입니다.
현재 3번째 반복 중입니다.
현재 4번째 반복 중입니다.
현재 5번째 반복 중입니다.
현재 6번째 반복 중입니다.
현재 7번째 반복 중입니다.
현재 8번째 반복 중입니다.
현재 9번째 반복 중입니다.
현재 10번째 반복 중입니다.

 

while 조건문은 바로 옆의 소괄호 안에 조건부가 들어간다.

조건부에는 bool 타입 결과를 반환할 수 있어야 하며, 이 값이 참인 동안 실행 블록 { } 의 내용을 반복 한다.

for 문과 달리 소괄호를 공백으로 둘 수 없다.

 

앞서 for 문에서 정리하지 못했지만, 만약 의도적으로 무한히 반복하는 반복문을 정의하고 싶다면 간단하게 다음과 같이 할 수 있다.

 

package me.xxxelppa.study.week04;
 
public class Exam_016 {
    public static void main(String[] args) {
        /*
         * for 문의 무한 반복 처리
         */
        for( ; ; ) {
            // 이 실행 블록을 무한히 반복한다.
        }
    
        /*
         * while 문의 무한 반복 처리
         */
        while (true) {
            // 이 실행 블록을 무한히 반복한다.
        }
    }
}

 

(위와 같이 작성하면 IDE에 따라 다를 수 있지만, 8라인의 for 문이 무한반복하는 것을 감지하고

  15라인에 실행이 도달할 수 없음을 알려주는 오류가 발생할 수 있다.)

 

for 문에 비해 while 문을 사용할 때 유난히 무한루프 (무한반복) 상태에 빠지도록 작성 할 가능성이 높다.

개인적으로 while 문 보다는 for 문을 더 많이 사용하는 편이다.

 

 

반복문이 무한히 실행되는 경우 작성하는 방법에 대해 알아 보았다.

과연 그런 경우가 얼마나 있을까?

당장 생각나는 경우는 채팅 프로그램이다.

채팅 프로그램 같은 경우 나와 상대방이 존재하고, 상대방이 전달하는 메시지를 내 화면에 출력해 주어야 하기 때문인데

언제 상대방의 메시지가 전달되어 올 지 알 수 없기 때문에 항상 대기 상태를 유지하기 위해 의도적으로 무한루프를 사용할 수 있다.

 

그럼 이런 프로그램은 강제로 종료해야만 끝이 나는데 과연 좋은 방법일까.

 

그래서 반복문에서 사용할 수 있는 두 가지 키워드가 존재한다.

하나는 break 이고 다른 하나는 continue 이다.

 

 

우선 break 의 사용부터 알아보자.

break 는 단어가 주는 느낌 그대로 반복을 종료하고 싶을 때 사용할 수 있다.

 

package me.xxxelppa.study.week04;
 
public class Exam_017 {
    public static void main(String[] args) {
        
        //사용자가 입력한 값이라고 가정한다.
        int userInput = 10;
        
        System.out.println(getSum(userInput));
        
    }
    
    public static int getSum(int target) {
        int result = 0;
        int adder = 1;
        
        for(;;) {
            if(adder > target) break;
            
            result += adder++;
        }
        
        return result;
    }
}

 

55

 

17라인에서 의도적으로 무한루프를 발생 시켰다.

그리고 18라인에서 adder 값이 target 보다 크다고 판단이 되면 (즉, 조건이 참인 경우) break 를 실행한다.

 

이 키워드는 ​현재 가장 가까운 반복문을 종료한다고 생각하면 된다.

현재 가장 가까운 반복문은 17라인에 정의 되어있으므로 adder가 11이 된 다음 반복문을 탈출 (종료)하고 result 를 반환한다.

 

while 문에서도 동일하다.

 

package me.xxxelppa.study.week04;
 
public class Exam_018 {
    public static void main(String[] args) {
        
        //사용자가 입력한 값이라고 가정한다.
        int userInput = 10;
        
        System.out.println(getSum(userInput));
        
    }
    
    public static int getSum(int target) {
        int result = 0;
        int adder = 1;
    
        while (true) {
            
            if (adder > target) break;
        
            result += adder++;
        }
        
        return result;
    }
}

 

55

 

주의할 것은 반복문을 중첩해서 사용했을 경우, 실행 블록을 혼동하면 의도와는 전혀 다르게 동작할 수 있다.

조금이라도 예방 할 수 있는 방법은 들여쓰기 (indent)를 잘하고, 실행 블록 { } 을 잘 작성해 주는 것 뿐이다.

 

 

그럼 break 말고 continue 는 무엇을 하는 키워드 일까.

 

이 키워드를 만나면 for 문의 경우 증감부를 거쳐 조건부로 바로 이동하고, while 문의 경우 조건부로 바로 이동한다.

위에 작성한 예제와 동일한데, 짝수인 경우만 더하고 싶다면 다음과 같이 할 수 있다.

 

package me.xxxelppa.study.week04;
 
public class Exam_019 {
    public static void main(String[] args) {
        
        //사용자가 입력한 값이라고 가정한다.
        int userInput = 10;
        
        System.out.println(getSum(userInput));
        
    }
    
    public static int getSum(int target) {
        int result = 0;
        
        for(int adder = 1; adder <= target; ++adder) {
            if(adder % 2 != 0) continue;
            
            result += adder;
        }
        
        return result;
    }
}

1부터 10까지의 합 중에서 짝수인 경우에만 더해서 반환 하도록 작성한 코드이다.

17라인에서 나머지 연산자를 사용해 2로 나눈 값이 0이 아니라면 홀수임을 뜻하므로

continue 를 만나 다음 실행 블록을 더이상 진행하지 않고 16라인의 증감부로 바로 이동한다.

 

while 문에서 continue의 의미는 동일하므로 예제를 생략한다.

 

마지막으로 do while 문이다.

do while 문은 while 문을 알면 쉽게 이해할 수 있다.

 

기본적인 사용 방법은 다음과 같다.

 

package me.xxxelppa.study.week04;
 
public class Exam_020 {
    public static void main(String[] args) {
        
        do {
            System.out.println("while 반복문의 실행 조건이 false 로 판별 되어도");
            System.out.println("do 블록을 무조건 한 번은 실행 합니다.");
        } while (false);
        
    }
}

 

while 반복문의 실행 조건이 false 로 판별 되어도
do 블록을 무조건 한 번은 실행 합니다.

 

while 문의 조건부를 확인하는 것처럼 똑같이 동작 하지만, 차이가 있다면 while 의 조건이 참/거짓 여부에 상관없이

무조건 한 번은 실행해야 하는 내용이 있을 경우 사용할 수 있는 반복문이다.

 

그 외의 모든 내용은 (break, continue 등) 모두 동일하게 적용 된다.

 

 

 

과제1 : live-study 대시 보드를 만드는 코드를 작성하세요.


 - 깃헙 이슈 1번부터 18번까지 댓글을 순회하며 댓글을 남긴 사용자를 체크 할 것.
 - 참여율을 계산하세요. 총 18회에 중에 몇 %를 참여했는지 소숫점 두자리가지 보여줄 것.
 - Github 자바 라이브러리를 사용하면 편리합니다.
 - 깃헙 API를 익명으로 호출하는데 제한이 있기 때문에 본인의 깃헙 프로젝트에 이슈를 만들고 테스트를 하시면 더 자주 테스트할 수 있습니다.


남겨주신 링크를 통해 라이브러리를 사용 해봤다.

 

 

 

링크를 클릭하니 위와 같은 사이트가 열렸다.

 

 

최근 버전 중에 사용자가 많은 버전을 사용해보기로 했다.

 

 

예제 코드를 작성하는 프로젝트가 maven을 사용하고 있지 않기 때문에, 정보만 참고 했다.

 

 

IDE 설정에 보니 maven 저장소에서 jar를 바로 내려받을 수 있는 기능이 있었다.

 

 

 

작업이 끝나면 위와 같이 외부 라이브러리에 추가된 것을 확인할 수 있다.

 

 

그리고 나서 샘플 코드를 작성 해보았더니 오류가 발생했다.

사진으로는 잘 안보이지만 읽어보니 제발 personal access token 을 만들어 달라는 간곡한 부탁과 함께

관련된 문서를 볼 수 있는 링크가 있었다. (친절하기도 하셔라)

 

링크에 들어가면 github 에서 personal access token을 생성하는 방법이 아주 친절하게 나와있다.

 

github api for Java 링크에서 나와있던 방법 중, To connect via Personal access token 방법을 사용했다.

 

package me.xxxelppa.study.week04;
 
import org.kohsuke.github.*;
 
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
 
public class Homework_001 {
    private static final int ISSUE_COUNT = 18;
    
    public static void main(String[] args) throws IOException {
        GitHub github = new GitHubBuilder().withOAuthToken("my_personal_token").build();  // 직접 생성한 토큰사용
        GHRepository repository = github.getRepository("nimkoes/live-study").getSource();
        GHIssue issue;
        List<GHIssueComment> comments;
        
        HashSet<String> hs;
        HashMap<String, Integer> hm = new HashMap<>();
        
        for (int i = 1; i <= ISSUE_COUNT; ++i) {
            issue = repository.getIssue(i);
            comments = issue.getComments();
            hs = new HashSet<>();
            
            String name = "";
            int size = comments.size();
            for (int j = 0; j < size; ++j) {
                try {
                    name = comments.get(j).getUser().getName();
                    if(name != null && !hs.contains(name)) hm.put(name, hm.getOrDefault(name, 0) + 1);
                } catch (IOException e) {
                    System.out.println("작업 도중 오류가 발생 하였습니다. [" + i + " 주차, 이름 = " + name + "]");
                }
            }
        }
    
        System.out.println("========== 참여율 ==========");
        hm.forEach((s, i) -> System.out.printf("%-20s -> %.2f%%\n", s, (i / (float)ISSUE_COUNT * 100)));
    }
}

 

이 방법이 맞는지 모르겠지만.. 실행 결과 일부를 첨부하면 다음과 같다.

 

 

되도록 텍스트로 가져오려 했지만, 공백 때문인지 여백이 들쭉날쭉해져서 위쪽 몇 개만 캡처 했다.

전문을 다 가져오는게 큰 의미가 없을 거라 생각했다.

 

 

github 사용이 서툴어서 어떻게 하면 좋을지 고민 했는데, 운이 좋게도 내 저장소에 fork 한 다음

15라인 처럼 .getSource() 메소드를 사용하니 원본(?) 저장소에 접근이 가능했다.

 

처음에 그냥 했을 때는 댓글을 여러 번 남긴 경우가 있었는지, 4 이상의 값을 출력하기도 해서

동일 주차에 댓글이 2개 이상이면 무시하도록 했다. (25라인, 31라인)

그리고 댓글을 달았는지 여부로 참여율만 구하는 것이 목적이기 때문에, 굳이 주차 별 참여 여부에 대한 내용은 고려하지 않았다.

 

전체 issue의 개수를 가져오고 싶었는데, open 된 상태의 issue만 가져올 수 있는 것 같아서 고민하다가

나는 이미 18주차까지 있다는 것을 알고 있기 때문에 깊게 고민하지 않고 final 변수를 사용했다.

 

 

 

과제2 : LinkedList를 구현하세요.


 - LinkedList에 대해 공부하세요.
 - 정수를 저장하는 ListNode 클래스를 구현하세요.
 - ListNode add(ListNode head, ListNode nodeToAdd, int position)를 구현하세요.
 - ListNode remove(ListNode head, int positionToRemove)를 구현하세요.
 - boolean contains(ListNode head, ListNode nodeTocheck)를 구현하세요.


List 계열 자료구조의 특징은 순서를 가진다는 것이다.

그 중에 LinkedList 는 리스트(목록)를 구성하는 각 노드(원소 또는 요소)가 링크를 사용하여 연결되어 있는 자료 구조이다.

 

예를 들면 다음과 같이 생겼다고 상상할 수 있다.

 

 

리스트를 구성할 때 크게 배열을 사용하는 방법과 링크를 사용하는 방법이 있다.

배열 구조를 사용했을 때와 링크를 사용했을 때의 장단이 있기 때문에 무엇이 더 좋다고 하기는 어렵다.

그저 현재 해결하려는 문제 상황에 적절한 자료구조를 선택해서 사용하면 될 것 같다.

(예를 들면 검색(탐색)이 빈번하게 발생 하는지, 갱신(입력, 수정, 삭제)이 빈번하게 발생하는지 등)

 

현재 과제에서 구현해야 하는 기능에 초점을 맞춰 LinkedList 에서 데이터 추가 삭제 및 검색이 어떻게 이루어 지는지 알아보자.

 

기본적으로 데이터가 위 그림처럼 링크로 연결된 형태로 저장되어 있다고 할 때

임의의 위치에 데이터의 삽입은 다음과 같은 과정을 거쳐야 한다

 

 

 

삽입하고자 하는 위치를 가리키는 값을 복사 한다.

 

 

새로 삽입한 데이터를 포함할 수 있도록 다음 노드 값을 바꿔준다.

삭제는 조금 더 간단하다.

 

 

삭제 대상이 가리키고 있는 다음 노드에 대한 정보를

자신을 가리키고 있는 노드의 다음 대상으로 넣어주면 된다.

그리고 삭제 대상이 null 참조를 하게 하면 삭제가 완료 된다.

 

 

이 문제 상황을 잘 이해하지 못한 것인지 의심이 들지만..

head 역할을 하는 ListNode 가 있고 여기에 값과 다음 노드를 가리킬 값이 있어야 할 것 같은데..

그러니까 다음과 같이 되어야 하는 것 아닌지 생각이 들었다.

 

 

그런데 문제의 add 와 remove 메소드 시그니처를 보면 추가하고 삭제하는데 ListNode 를 사용한다.

그래서 어쨌든 ListNode는 int 타입의 값을 담을 변수와 다음 노드를 가리키는 ListNode 타입의 변수를 가지고 있어야 한다.

또한 linear 하게 연결 되어 있지만, 중간에 삽입된 노드를 head 를 통하지 않고 직접 접근이 가능하다면

그 위치를 새로운 head 로 인식하여 추가 삭제 작업이 가능할 수 있다.

 

지금 다시 생각해보니, 당면한 문제는 길이 0인 리스트를 만들 수 없는 형태로 구현 했다는 것이다.

데이터 범위도 정수이기 때문에 특정 정수 값을 (예를 들면 -1) head 노드라고 판단하도록 사용할 수도 없다.

 

여러 고민 끝에 다음과 같이 다시 구현 하였다.

 

package me.xxxelppa.study.week04;
 
import java.util.Objects;
 
public class ListNode {
    
    private int data;
    private ListNode next;
    private boolean isHead;
    
    public int getData() {
        return this.data;
    }
    
    /*
     * 기본 생성자를 사용할 경우 head 노드 생성
     */
    public ListNode() {
        this.data = 0;
        this.next = null;
        this.isHead = true;
    }
    
    /*
     * 생성자에 데이터가 넘어오면 데이터 노드 생성
     */
    public ListNode(int data) {
        this.data = data;
        this.next = null;
        this.isHead = false;
    }
    
    /*
     * 크기를 반환하는 메소드
     */
    public int size() {
        if(!this.isHead) {
            System.out.println("head 노드가 아니므로 길이를 반환할 수 없습니다.");
            return -1;
        }
        
        int size = 0;
        ListNode ln = this;
        while(ln.next != null) {
            ++size;
            ln = ln.next;
        }
        
        return size;
    }
    
    /*
     * 입력 받은 position 에 따라 후속 작업이 가능한지 검사
     * 1. head 노드가 아닌 경우 false 반환
     * 2. position 이 음수인 경우 false 반환
     * 3. position 이 현재 리스트의 전체 길이를 넘길 경우 false 반환
     */
    private boolean basicValidation(int pos) {
        if(!this.isHead) {
            System.out.println("head 노드를 기준으로만 처리할 수 있습니다.");
            return false;
        }
        
        if(pos < 0) {
            System.out.println("음수 위치에서 값을 처리할 수 없습니다.");
            return false;
        }
        
        if(size() < pos) {
            System.out.println("현재 리스트 길이보다 큰 위치에서 처리할 수 없습니다.");
            return false;
        }
        
        return true;
    }
    
    /*
     * 요소를 추가하는 add 메소드
     * null 을 반환하면 추가할 수 없음을 의미
     * 성공적으로 추가 해을 경우 추가한 노드를 반환
     */
    public ListNode add(ListNode head, ListNode nodeToAdd, int position) {
        if(!basicValidation(position)) {
            return null;
        }
        
        while(--position >= 0) {
            head = head.next;
        }
        
        nodeToAdd.next = head.next;
        head.next = nodeToAdd;
        
        return nodeToAdd;
    }
    
    /*
     * 특정 위치의 노드를 삭제
     * null을 반환하면 삭제할 수 없음을 의미
     * 성공적으로 삭제한 경우 삭제한 노드를 반환
     */
    public ListNode remove(ListNode head, int positionToRemove) {
        if(!basicValidation(positionToRemove)) {
            return null;
        }
        
        if(size() == 0) {
            System.out.println("데이터가 없습니다.");
            return null;
        }
        
        ListNode deleteNode = head.next, beforeNode = head;
        
        while(--positionToRemove > 0) {
            beforeNode = deleteNode;
            deleteNode = deleteNode.next;
        }
        
        beforeNode.next = deleteNode.next;
        
        return deleteNode;
    }
    
    /*
     * 노드가 포함되어 있는지 확인
     */
    public boolean contains(ListNode head, ListNode nodeTocheck) {
        boolean result = false;
        
        if(!head.isHead) {
            System.out.println("head 노드가 아니면 작업을 처리할 수 없습니다.");
            return result;
        }
        
        do {
            if(head.equals(nodeTocheck)) {
                result = true;
                break;
            }
            head = head.next;
        } while(head != null);
        
        return result;
    }
    
    @Override
    public boolean equals(Object o) {
        if(this == o) return true;
        if(o == null || getClass() != o.getClass()) return false;
        ListNode listNode = (ListNode) o;
        return this.data == listNode.data && Objects.equals(this.next, listNode.next);
    }
}

 

간단한 테스트 코드도 작성 해보았다.

 

package me.xxxelppa.study.week04;
 
import org.junit.jupiter.api.Test;
 
import static org.junit.jupiter.api.Assertions.*;
 
class ListNodeTest {
    
    @Test
    public void addTest() {
        ListNode headNode = new ListNode();
        ListNode dataNode = new ListNode(10);
        ListNode newDataNode = new ListNode(20);
    
        // head 노드를 참조하지 않으면 데이터 추가를 할 수 없고 시도할 경우 null 을 반환
        assertNull(dataNode.add(dataNode, new ListNode(20), 0));
        
        // head 노드를 참조하면 데이터를 추가할 수 있음
        assertNotNull(headNode.add(headNode, dataNode, 0));
        
        // head 노드에 데이터 노드가 성공적으로 추가되면 추가한 노드를 반환
        assertEquals(newDataNode, headNode.add(headNode, newDataNode, 1));
        
        // 범위 밖의 위치에 값 추가 시도시 null 반환
        assertNull(headNode.add(headNode, new ListNode(40), 4));
        assertNull(headNode.add(headNode, new ListNode(40), -1));
    }
    
    @Test
    public void removeTest() {
        ListNode headNode = new ListNode();
        
        // 삭제 할 데이터 세팅
        for(int i = 1; i < 5; ++i) {
            headNode.add(headNode, new ListNode(i * 10), (i-1));
        }
    
        // 성공적으로  노드를 삭제하면, 삭제된 노드를 반환
        assertEquals(20, headNode.remove(headNode, 2).getData());
    
    
        // 범위 밖의 위치 노드 삭제 시도시 null 반환
        assertNull(headNode.remove(headNode, 4));
        assertNull(headNode.remove(headNode, -1));
    }
    
    @Test
    public void containsTest() {
        ListNode headNode = new ListNode();
        ListNode containCheckNode = new ListNode(40);
        
        headNode.add(headNode, new ListNode(10), 0);
        headNode.add(headNode, new ListNode(20), 1);
        headNode.add(headNode, new ListNode(30), 2);
        headNode.add(headNode, containCheckNode, 3);
    
        assertTrue(headNode.contains(headNode, containCheckNode));
        assertFalse(headNode.contains(headNode, new ListNode(99)));
    }
}

 

 

 

과제3 : Stack을 구현하세요.


 - int 배열을 사용해서 정수를 저장하는 Stack을 구현하세요.
 - void push(int data)를 구현하세요.
 - int pop()을 구현하세요.


Stack은 FILO (First In Last Out) 으로 동작하는 자료구조이다.

하노이의 탑을 떠올리면 된다.

 

package me.xxxelppa.study.week04;
 
class MyStack {
    int[] myStack;
    int stackSize;
    int dataCount;
    
    public MyStack(int data) {
        this.stackSize = 10;
        this.dataCount = 1;
        this.myStack = new int[stackSize];
        
        this.myStack[0] = data;
    }
    
    public void push(int data) {
        // 스택 크기를 초과할 경우 10씩 늘려준다.
        if(this.stackSize == this.dataCount + 1) {
            int[] newStack = new int[stackSize + 10];
            for (int i = 0; i < stackSize; ++i) newStack[i] = this.myStack[i];
            stackSize += 10;
            this.myStack = newStack;
        }
        
        this.myStack[this.dataCount++] = data;
    }
    
    public int pop() {
        if (this.dataCount == 0) {
            System.out.println("더 이상 데이터가 없습니다");
            return -1;
        }
        return myStack[--this.dataCount];
    }
    
    public void print() {
        for (int i = 0; i < this.dataCount; ++i) {
            System.out.println(i + " :: " + myStack[i]);
        }
    }
}

 

확인해보기 위한 테스트 코드.

 

package me.xxxelppa.study.week04;
 
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
 
import static org.junit.jupiter.api.Assertions.*;
 
class MyStackTest {
    
    MyStack myStack;
    
    @BeforeEach
    public void before() {
        myStack = new MyStack(10);
        for (int i = 20; i < 100; i += 10) myStack.push(i);
    }
    
    @Test
    public void pushTest() {
        // 범위를 초과해서 값을 넣어도 오류가 발생하지 않는다.
        assertDoesNotThrow(() -> myStack.push(999));
        assertDoesNotThrow(() -> myStack.push(888));
        assertDoesNotThrow(() -> myStack.push(777));
        assertDoesNotThrow(() -> myStack.push(666));
    }
    
    @Test
    public void popTest() {
        // 스택의 모든 값을 꺼내서 확인
        for (int i = 90; i > 0; i -= 10) {
            assertEquals(i, myStack.pop());
        }
        
        // 스택이 비어있는 상태에서 pop을 호출하면 -1을 반환한다.
        assertEquals(-1, myStack.pop());
        assertEquals(-1, myStack.pop());
        
    }
}

 

 

 

 

과제4 : 앞서 만든 ListNode를 사용해서 Stack을 구현하세요.


 - ListNode head를 가지고 있는 ListNodeStack 클래스를 구현하세요.
 - void push(int data)를 구현하세요.
 - int pop()을 구현하세요.


ListNodeStack 타입의 변수를 생성하면 내부에서 ListNode를 사용하고

push를 하면 마지막 위치에 노드를 삽입

pop을 하면 마지막 위치의 노드를 삭제하고 반환 하도록 했다.

 

package me.xxxelppa.study.week04;
 
public class ListNodeStack {
    ListNode head;
    
    public ListNodeStack() {
        head = new ListNode();
    }
    
    public ListNodeStack(int data) {
        this();
        head.add(head, new ListNode(data), head.size());
    }
    
    public void push(int data) {
        head.add(head, new ListNode(data), head.size());
    }
    
    /*
     * 반환할 데이터가 없는 경우 -1 반환
     */
    public int pop() {
        try {
            return head.remove(head, head.size()).getData();
        } catch (NullPointerException e) {
            return -1;
        }
    }
}

 

간단하게 작성해본 테스트 코드

 

package me.xxxelppa.study.week04;
 
import org.junit.jupiter.api.Test;
 
import static org.junit.jupiter.api.Assertions.*;
 
class ListNodeStackTest {
    
    @Test
    void pushTest() {
        // 길이 0 인 스택 생성
        ListNodeStack emptyListNodeStack = new ListNodeStack();
        emptyListNodeStack.push(10);
        assertEquals(10, emptyListNodeStack.pop());
    
        // 길이 1인 스택 생성
        ListNodeStack listNodeStack = new ListNodeStack(100);
        assertEquals(100, listNodeStack.pop());
    }
    
    @Test
    void popTest() {
        // 길이 1인 스택 생성
        ListNodeStack listNodeStack = new ListNodeStack(1000);
        assertEquals(1000, listNodeStack.pop());
        
        // 길이 0인 상태에서 pop 을 시도하면 -1 을 반환
        assertEquals(-1,listNodeStack.pop());
    }
}

 

 

 

 

과제5 : Queue를 구현하세요.


 - 배열을 사용해서 한번
 - ListNode를 사용해서 한번.


Queue는 FIFO (First In First Out) 로 동작하는 자료구조 이다.

외나무다리를 건넌다고 생각하면 될 것 같다.

 

배열로 구현하는게 조금 고민이다.

ListNode를 사용할 경우 0번째를 제거하고 마지막에 값을 추가하는 것으로 구현할 수 있다.

배열은 인덱스가 가장 작은 값을 반환하고, 가장 마지막 위치에 넣으면 되는데

문제는 배열로 만든 큐에서 값을 꺼내면 값이 하나씩 앞으로 당겨와야하는 갱신 작업이 필요하다.

 

값을 뺄 때마다 갱신하는 작업을 할지, 아니면 갱신 하지 않고 배열 크기를 무한정 늘리면서 참조하는 인덱스 값만 조정할지..

아무래도 크기를 정해 놓고, 일정 횟수 이상 값을 뺐을 경우 재 정렬 하도록 하는 것이 나을 것 같다.

 

우선 배열을 사용해서 구현해 보았다.

 

package me.xxxelppa.study.week04;
 
public class QueueUsingArray {
    
    private final int EXTEND_SIZE = 10;
    private int[] myQueue;
    private int headPos;
    private int tailPos;
    
    public QueueUsingArray() {
        this.myQueue = new int[EXTEND_SIZE];
        this.headPos = 0;
        this.tailPos = 0;
    }
    
    /*
     * pop 실행 중 head 위치가 EXTEND_SIZE 에 다다르면 호출
     * 배열의 0번째부터 값을 다시 채우고, headPos 와 tailPos 다시 할당
     */
    public void resetQueue() {
        for(int i = headPos, j = 0; i <= tailPos; ) myQueue[j++] = myQueue[i++];
        tailPos -= headPos;
        headPos = 0;
    }
    
    /*
     * Queue 의 크기를 EXTEND_SIZE 만큼 확장
     */
    public int[] extendQueue() {
        int[] newQueue = new int[myQueue.length + EXTEND_SIZE];
        for(int i = 0; i < myQueue.length; ++i) newQueue[i] = myQueue[i];
        return newQueue;
    }
    
    // 배열의 마지막 위치에 값 추가
    public void push(int data) {
        if(tailPos + 1 == myQueue.length) myQueue = extendQueue();
        this.myQueue[tailPos++] = data;
    }
    
    /*
     * 반환할 데이터가 없는 경우 -1 반환
     */
    public int pop() {
        if(headPos == tailPos) return -1;
        if(headPos > EXTEND_SIZE) resetQueue();
        return myQueue[headPos++];
    }
    
    // Queue 에 들어있는 데이터의 수를 반환
    public int size() {
        return this.tailPos - this.headPos;
    }
}

 

간단한 테스트 코드를 작성 해보았다.

 

package me.xxxelppa.study.week04;
 
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
 
import static org.junit.jupiter.api.Assertions.*;
 
class QueueUsingArrayTest {
    
    QueueUsingArray queueUsingArray;
    
    @BeforeEach
    public void before() {
        queueUsingArray = new QueueUsingArray();
    
        queueUsingArray.push(10);
        queueUsingArray.push(20);
        queueUsingArray.push(30);
        queueUsingArray.push(40);
        queueUsingArray.push(50);
        queueUsingArray.push(60);
        queueUsingArray.push(70);
        queueUsingArray.push(80);
        queueUsingArray.push(90);
        
    }
    
    @Test
    public void pushTest() {
        assertEquals(9, queueUsingArray.size());
        
        queueUsingArray.push(100);
        queueUsingArray.push(110);
        queueUsingArray.push(120);
        
        // 기본 크기 10을 초과해도 자동으로 확장하여 데이터 입력이 가능
        assertEquals(12, queueUsingArray.size());
    }
    
    @Test
    public void popTest() {
        // 들어있는 데이터 전부 소모
        for(int i = 0; i < 9; ++i) {
            queueUsingArray.pop();
        }
        
        // 더이상 데이터가 없는데 pop 을 시도하면 -1을 반환
        assertEquals(0, queueUsingArray.size());
        assertEquals(-1, queueUsingArray.pop());
    }
    
}

 

지금 생각해보니 큐의 크기가 한 번 커지면 작아지지 않는다.

이런 것들을 포함해서 다양한 문제가 있지만.. 지금은 개념을 이해하는데 집중해서 간단하게 동작하는 정도로 구현하는 것에 만족..? 해야겠다.

 

마지막으로 ListNode 를 사용해서 Queue를 구현해본 코드이다.

 

package me.xxxelppa.study.week04;
 
public class QueueUsingListNode {
    ListNode head;
    
    public QueueUsingListNode() {
        head = new ListNode();
    }
    
    public void push(int data) {
        head.add(head, new ListNode(data), head.size());
    }
    
    public int pop() {
        try {
            return head.remove(head, 0).getData();
        } catch (NullPointerException e) {
            return -1;
        }
    }
}

 

ListNode를 사용하여 Stack을 구현했을 때와 거의 동일하다.

다른 점이 있다면, pop을 하여 데이터를 가져올 때 Stack 과 달리 선두에 있는 데이터를 가져온다는 것이다.

 

 

 

 

728x90
728x90

 

# 자바가 제공하는 다양한 연산자를 학습하세요.


# 학습할 것

  • 산술 연산자
  • 비트 연산자
  • 관계 연산자
  • 논리 연산자
  • instanceof
  • assignment(=) operator
  • 화살표(->) 연산자
  • 3항 연산자
  • 연산자 우선 순위
  • (optional) Java 13. switch operator

각 주제에 대해 본격적으로 알아보기 전에 연산자 관련하여 공통적인 내용에 대해 한 번 정리하려 한다.

 

<용어 정의>

연산 (operations) : 프로그램에서 데이터를 처리하여 결과를 산출하는 것

연산자 (operator) : 연산에 사용되는 표시나 기호

피연산자 (operand) : 연산의 대상이 되는 데이터

연산식 (expressions) : 연산자와 피연산자로 연산의 과정을 기술한 것

 

연산자 종류 연산자 피연산자 수 결과값 설명
산술 +, -, *, /, % 이항 숫자 사칙연산 및 나머지 계산
부호 +, - 단항 숫자 음수와 양수의 부호
문자열 + 이항 문자열 두 문자열을 연결
대입 =, +=, -=, *=, /=, %=, &=, 
^=, |=, <<=, >>=, >>>=
이항 다양 우변의 값을 좌변의 변수에 대입
증감 ++, -- 단항 숫자 1만큼 증가/감소
비교(관계) ==, !=, >, <, >=, <=,
instanceof
이항 boolean 값의 비교
논리 !, &, |, &&, || 단항, 이항 boolean 논리적 NOT, AND, OR 연산
조건 (조건식) ? A : B 삼항 다양 조건식에 따라 A 또는 B 중 하나를 선택
비트 ~, &, |, ^ 단항, 이항 숫자, boolean 비트 NOT, AND, OR, XOR 연산
쉬프트 >>, <<, >>> 이항 숫자 비트를 좌측/우측으로 밀어서 이동

(** 한빛 미디어, 이것이 자바다. 신용권의 Java 프로그래밍 정복 내용 참고)

 

 

 

산술연산자


일반적으로 산술 연산은 덧셈, 뺄셈, 곱셈, 나눗셈의 사칙 연산을 뜻한다.

자바에서 산술 연산은 사칙연산과 나머지 연산을 포함한 다섯 가지 연산을 뜻한다.

 

덧셈과 뺄셈 그리고 곱셈은 일반적으로 알고 있는 수학에서 계산과 동일하다.

나눗셈과 나머지 연산은 처음 프로그래밍을 접한다면 다소 생소할 수 있다.

 

산술 연산에 대한 예를 살펴보기 전에 2주차에서 살펴봤던 타입 캐스팅과 타입 프로모션을 다시 한 번 생각해볼 필요가 있다.

 

요약하자면,

타입 캐스팅은 원본 데이터의 데이터 타입 표현 범위를 모두 표현하지 못하는 데이터 타입으로 만들어진 변수에 값을 넣을 때 발생하는 것이고

타입 프로모션은 원본 데이터의 데이터 타입 표현 범위를 모두 표현할 수 있는 데이터 타입으로 만들어진 변수에 값을 넣을 때 발생하는 것이다.

 

산술 연산에서는 타입 캐스팅과 타입 프로모션이 빈번히 발생할 수 있기 때문에, 데이터 타입에 따른 값의 변화에 주의해야 한다.

 

package me.xxxelppa.study.week03;
 
public class Exam_001 {
    public static void main(String[] args) {
        int v1 = 10;
        int v2 = 3;
    
        System.out.println("v1 + v2 = " + (v1 + v2));
        System.out.println("v1 - v2 = " + (v1 - v2));
        System.out.println("v1 * v2 = " + (v1 * v2));
        System.out.println("v1 / v2 = " + (v1 / v2));
        System.out.println("v1 % v2 = " + (v1 % v2));
        
    }
}

 

v1 + v2 = 13
v1 - v2 = 7
v1 * v2 = 30
v1 / v2 = 3
v1 % v2 = 1

 

예시로 작성한 코드에서 보면 알 수 있듯, 정수형 자료형을 사용해서 연산을 했기 때문에 정수 표현 범위 내에서만 결과를 만들어낼 수 있다.

그래서 나눗셈과 나머지 연산을 보면, 정확히 그 몫과 나머지를 정수형으로 자동 변환 되어 결과를 출력한 것을 볼 수 있다.

 

이것을 실수형 자료형을 사용하면 결과가 조금 다르게 나온다.

 

package me.xxxelppa.study.week03;
 
public class Exam_002 {
    public static void main(String[] args) {
        double v1 = 10;
        double v2 = 3;
    
        System.out.println("v1 + v2 = " + (v1 + v2));
        System.out.println("v1 - v2 = " + (v1 - v2));
        System.out.println("v1 * v2 = " + (v1 * v2));
        System.out.println("v1 / v2 = " + (v1 / v2));
        System.out.println("v1 % v2 = " + (v1 % v2));
        
    }
}

 

v1 + v2 = 13.0
v1 - v2 = 7.0
v1 * v2 = 30.0
v1 / v2 = 3.3333333333333335
v1 % v2 = 1.0

 

실수 표현 범위를 가질 수 있기 때문에 소수점 아래 값도 계산되어 결과가 나오는 것을 볼 수 있다.

 

 

 

비트 연산자


개인적으로 비트 연산을 자주 사용하는 상황이 없었기 때문에 오랜 기억을 떠올려야 한다.

비트 연산은 1과 0을 가지고 이루어진다.

일반적으로 0이 거짓, false를 상징하고, 그 외의 모든 값은 true를 상징한다.

 

~ 은 단항 연산을 하며 부정, not 을 뜻한다. 그래서 1은 0으로 0은 1로 변환한다. (NOT)

& 는 이항 연산자로 양쪽 항의 값이 모두 1인 경우 1을 반환한다. (AND)

| 는 이항 연산자로 양쪽 항 중 하나라도 1이면 1을 반환한다. (OR)

^ 는 이항 연산자로 양쪽 한의 값이 서로 다를 때 1을 반환 한다. (XOR, exclusive or)

 

비트 연산을 할 때 '진리표'라는 것을 작성 하기도 한다.

진리표는 모든 입출력 경우에 대한 참거짓 결과를 논리값으로 정리한 표이다.

 

각 비트 연산에 대한 진리표는 다음과 같다.

 

~ (NOT)
입력1 결과
1 0
0 1

비트를 반전 시킨다.

 

& (AND)
입력1 입력2 결과
1 1 1
1 0 0
0 1 0
0 0 0

둘 다 참(1)인 경우 참(1)을 반환 한다.

 

| (OR)
입력1 입력2 출력
1 1 1
1 0 1
0 1 1
0 0 0

하나라도 참(1)인 경우 참(1)을 반환 한다.

 

^ (XOR)
입력1 입력2 결과
1 1 0
1 0 1
0 1 1
0 0 0

서로 다른 경우 참(1)을 반환 한다.

 

비트 연산을 다양한 곳에서 활용할 수 있겠지만, 지금 당장 생각 나는 건 네트워크의 서브넷 마스크이다.

 

숫자에 대해 비트 연산을 하면 어떻게 되는지 예시를 살펴보자.

 

package me.xxxelppa.study.week03;
 
public class Exam_004 {
    public static void main(String[] args) {
        // 정수형 자료형 int는 8바이트, 즉 32비트로 표현 한다.
        int v1 = 10;    // 00000000 00000000 00000000 00001010
        int v2 = 15;    // 00000000 00000000 00000000 00001111
        
        /*
         * ~ 연산을 하면 모든 비트를 반전한다.
         * 
         * 11111111 11111111 11111111 11110101
         * MSB가 1 -> 음수를 뜻함
         * 2의 보수를 취한 값에 -부호를 붙인 값을 반환
         *
         * 00000000 00000000 00000000 00001010    -> 모든 비트 반전
         * 00000000 00000000 00000000 00001011    -> 1을 더함
         *
         * 1011 은 10진수 11이므로 -11을 뜻함
         */
        System.out.println(~v1);        // -11
        
        /*
         * 00000000 00000000 00000000 00001010
         * 00000000 00000000 00000000 00001111
         * -------------------------------------
         * 양쪽 모두 1일 때 1을 반환
         * 00000000 00000000 00000000 00001010
         *
         * 1010 은 10진수 10을 뜻함
         */
        System.out.println(v1 & v2);    // 10
    
        /*
         * 00000000 00000000 00000000 00001010
         * 00000000 00000000 00000000 00001111
         * -------------------------------------
         * 한쪽이라도 1이면 1을 반환
         * 00000000 00000000 00000000 00001111
         *
         * 1111 은 10진수 15을 뜻함
         */
        System.out.println(v1 | v2);    // 15
    
        /*
         * 00000000 00000000 00000000 00001010
         * 00000000 00000000 00000000 00001111
         * -------------------------------------
         * 서로 다를 때 1을 반환
         * 00000000 00000000 00000000 00000101
         *
         * 101 은 10진수 5을 뜻함
         */
        System.out.println(v1 ^ v2);    // 5
        
    }
}

 

-11
10
15
5

 

알고리즘 문제를 풀 때도 가끔 비트연산을 활용하면 생각보다 문제를 쉽게 풀 수 있을때가 있으니

자주 사용하지 않더라도 대략 어떻게 동작하는지 알아두면 좋을것 같다.

 

 

728x90

 

 

관계 연산자


관계 연산자는 이 연산자를 중심으로 양쪽의 값이 어떤 관계를 갖는지 확인하는 연산이다.

예를 들면, 두 값이 같은지, 어느 쪽 같이 더 큰지, 작은지 등을 알아보는데 사용 한다.

종류는 다음과 같다.

==, !=, >, <, >=, <=, instanceof

 

연산자 이름 설명
== 같음 양쪽 값이 같으면 참, 다르면 거짓
!= 같지 않음 양쪽 값이 다르면 참, 같으면 거짓
> 보다 큼 왼쪽 값이 크면 참, 같거나 작으면 거짓
>= 보다 크거나 같음 왼쪽 값이 크거나 같으면 참, 작으면 거짓
< 보다 작음 왼쪽 값이 작으면 참, 같거나 크면 거짓
<= 보다 작거나 같음 왼쪽 값이 작거나 같으면 참, 크면 거짓
instanceof   왼쪽 참조 변수 값이 오른쪽 참조 변수 타입이면 참, 아니면 거짓

 

package me.xxxelppa.study.week03;
 
public class Exam_005 {
    public static void main(String[] args) {
        // == 연산자
        System.out.println("10 == 10 : " + (10 == 10));   // true
        System.out.println("10 == 20 : " + (10 == 20));   // false
    
        // != 연산자
        System.out.println("10 != 10 : " + (10 != 10));   // false
        System.out.println("10 != 20 : " + (10 != 20));   // true
    
        // > 연산자
        System.out.println("10 > 20 : " + (10 > 20));    // false
        System.out.println("20 > 10 : " + (20 > 10));    // true
        
        // >= 연산자
        System.out.println("10 >= 10 : " + (10 >= 10));   // true
        System.out.println("10 >= 20 : " + (10 >= 20));   // false
        
        // < 연산자
        System.out.println("10 < 20 : " + (10 < 20));    // true
        System.out.println("20 < 10 : " + (20 < 10));    // false
        
        // <= 연산자
        System.out.println("10 <= 10 : " + (10 <= 10));   // true
        System.out.println("20 <= 10 : " + (20 <= 10));   // false
        
    }
}

 

10 == 10 : true
10 == 20 : false
10 != 10 : false
10 != 20 : true
10 > 20 : false
20 > 10 : true
10 >= 10 : true
10 >= 20 : false
10 < 20 : true
20 < 10 : false
10 <= 10 : true
20 <= 10 : false

 

instanceof 에 대해서는 클래스에 대해 정리해볼 때 따로 예시를 작성 해보려 한다.

그 외의 경우 수학에서 배웠던 것과 크게 다르지 않으며, '같음'과 '같지 않음'이 생긴 것이 조금 생소할 뿐이다.

 

 

 

논리 연산자


비트 연산과 비슷하지만 그 대상(피연산자)이 boolean 타입의 논리 값이라는 것이다.

! 는 논리적인 부정을 뜻하며, true를 false로, false를 true로 바꿔준다.

그 외 && (AND), || (OR) 연산은 비트 연산자에서 보았던 것과 같은 개념을 갖는다.

 

즉, &&는 양쪽 피연산자 모두 true 일 때 true 를 반환하고 그 외의 경우는 false 를 반환한다.

|| 는 양쪽 피연산자 중 하나라도 true이면 true 를 반환하고 그 외의 경우는 false 를 반환한다.

 

package me.xxxelppa.study.week03;
 
public class Exam_006 {
    public static void main(String[] args) {
        boolean myTrue = true;
        boolean myFalse = false;
        
        if (myTrue & myFalse)  System.out.println("if test 1 > myTrue 와 myFalse 는 모두 true 입니다.");
        if (myTrue | myFalse)  System.out.println("if test 2 > myTrue 와 myFalse 둘 중 하나는 true 입니다.");
        if (myTrue && myFalse) System.out.println("if test 3 > myTrue 와 myFalse 는 모두 true 입니다.");
        if (myTrue || myFalse) System.out.println("if test 4 > myTrue 와 myFalse 둘 중 하나는 true 입니다.");
        
        if (!myFalse) System.out.println("!myFalse 의 결과는 true 입니다.");
        
    }
}

 

if test 2 > myTrue 와 myFalse 둘 중 하나는 true 입니다.
if test 4 > myTrue 와 myFalse 둘 중 하나는 true 입니다.
!myFalse 의 결과는 true 입니다.

 

그렇다면 & 와 && 그리고 | 과 || 는 무엇이 다른걸까.

&&는 첫번째 조건이 참이 아니면 두번째 조건은 확인하지 않는다.

&는 첫번째 조건이 참이 아니어도 두번째 조건을 확인한다.

 

||는 첫번째 조건이 참이면 두번째 조건은 확인하지 않는다.

|는 첫번째 조건이 참이어도 두번째 조건을 확인한다.

 

간혹 && 와 || 연산을 사용할 때 truthy, falsy 하다라는 말을 사용하기도 한다.

 

 

 

instanceof


스터디 진행 중 클래스에 대해 학습해볼 때 알아보려고 했는데, 지금 알아봐야 할 것 같다.

사용 방법은 "(레퍼런스 타입 변수) instanceof (레퍼러스 데이터 타입)" 이며, 레퍼런스 타입 변수가 레퍼런스 타입의 데이터 타입인지 확인해보는 연산이다.

 

클래스 역시 사용자 정의 자료형이라 할 수 있기 때문에 포괄적으로 레퍼런스 데이터 타입 이라고 정리했다.

 

다양한 곳에서 활용할 수 있지만, 보통 레퍼런스 타입 변수가 레퍼런스 데이터 타입으로 타입 변환이 가능한지 확인하기 위해서 사용한다.

타입 변환이 가능 하다는 것은 여러가지 내용을 내포할 수 있다.

상속에 대해 정리해볼 때 잊지 않는다면 다시 한 번 이 내용을 언급 해야겠다.

 

우선 instanceof 를 사용하는 간단한 예제 코드를 작성해 보았다.

 

package me.xxxelppa.study.week03;
 
public class Exam_007 {
    public static void main(String[] args) {
        MyParents_0 myParents_0 = new MyParents_0();
        MyParents_1 myParents_1 = new MyParents_1();
        MyParents_2 myParents_2 = new MyParents_2();
    
        System.out.println("expect false :: " + (myParents_0 instanceof MyInterface));
        System.out.println("expect true  :: " + (myParents_1 instanceof MyInterface));
        System.out.println("expect true  :: " + (myParents_2 instanceof MyInterface));
    
        System.out.println("expect true  :: " + (myParents_1 instanceof MyParents_2));
        
        /*
         * instanceof 연산 결과가 true 일 경우
         * 해당 타입의 변수에 값을 할당할 수 있다.
         */
        if (myParents_1 instanceof MyInterface) {
            MyInterface myInterface = new MyParents_1();
            System.out.println("자신의 상위 타입의 변수에 값을 할당할 수 있다. :: " + (myInterface != null));
        }
    }
}
 
class MyParents_0 {}
class MyParents_1 extends MyParents_2 {}
class MyParents_2 implements MyInterface {}
interface MyInterface {}

 

expect false :: false
expect true  :: true
expect true  :: true
expect true  :: true
자신의 상위 타입의 변수에 값을 할당할 수 있다. :: true

 

 

 

 

assignment(=) operator


대입 또는 할당 연산자라고 부른다. 오른쪽의 피연산자를 왼쪽의 피연산자의 값으로 할당한다.

그렇기 때문에 왼쪽에는 변수가, 오른쪽엔 리터럴 또는 리터럴이 담긴 변수가 온다.

값을 초기화 한다고 표현하기도 한다.

 

등호(=) 만 사용하는 경우도 있지만, 다른 연산자를 함께 사용하여 문장의 길이를 줄이기도 한다.

다른 연산자를 함께 사용하면 다음과 같은 효과가 있다.

 

예를 들어 다른 연산자를 {?}라고 하면

variable {?}= literal;

 

이것은 다음 연산을 축약한 표현이 된다.

variable = variable {?} literal;

 

즉, 자기 자신에 대해 연산한 결과를 다시 자기 자신에 담을 경우 사용한다.

 

package me.xxxelppa.study.week03;
 
public class Exam_008 {
    public static void main(String[] args) {
        int v1 = 10;
        System.out.println(v1 += 20);
        System.out.println(v1);
    }
} 

 

30
30

 

7라인에서 v1 값을 한번 더 출력해본 이유는, 이러한 대입 연산을 하면 기존의 v1값을 덮어 쓴다는 것을 다시 한 번 확인해보기 위함이다.

다른 연산들은 수학에서도 보기 때문에 익숙하겠지만 >>, <<, >>> 과 같은 시프트 연산은 처음 볼 수 있다.

 

이들은 비트 이동 연산자로 말 그래도 비트를 이동하는 연산을 한다.

 

package me.xxxelppa.study.week03;
 
public class Exam_009 {
    public static void main(String[] args) {
        int v1, v2;
        
        System.out.println("============= << 연산 ============");
        v1 = 17;            // 00000000 00000000 00000000 00010001
        v2 = v1 << 3;       // 00000000 00000000 00000000 10001000
        System.out.println(v2); // 128 + 8 = 136
        System.out.println();
    
        System.out.println("========== 양수 >> 연산 ==========");
        v1 = 17;            // 00000000 00000000 00000000 00010001
        v2 = v1 >> 3;       // 00000000 00000000 00000000 00000010
        System.out.println(v2); // 2
        System.out.println();
    
        System.out.println("========== 음수 >> 연산 ==========");
        v1 = -17;           // 11111111 11111111 11111111 11101111
        v2 = v1 >> 3;       // 11111111 11111111 11111111 11111101
        System.out.println(v2); // -3
        System.out.println();
    
        System.out.println("========== 양수 >>> 연산 ==========");
        v1 = 17;            // 00000000 00000000 00000000 00010001
        v2 = v1 >>> 3;      // 00000000 00000000 00000000 00000010
        System.out.println(v2); // 2
        System.out.println();
    
        System.out.println("========== 음수 >>> 연산 ==========");
        v1 = -17;               // 11111111 11111111 11111111 11101111
        v2 = v1 >>> 3;          // 00011111 11111111 11111111 11111101
        System.out.println(v2); // 00100000 00000000 00000000 00000000 -> 536870912 :: 이 값에서 1을 빼면
                                // 00011111 11111111 11111111 11111111 -> 536870911 :: 이 값에서 2를 빼면
                                // 00011111 11111111 11111111 11111101 -> 536870909
        System.out.println();
    }
}

 

>> 이 것과 >>> 이 것의 차이는, 오른쪽으로 비트 이동을 할 때 MSB값으로 채우느냐 무조건 0으로 채우느냐 이다.
>> 이 연산의 경우 MSB 값으로 부족한 비트를 채우고, >>> 이 연산은 MSB 상관없이 무조건 0으로 값을 채워준다.

모든 비트 연산을 할 때, 밀려 나는 비트는 전부 버려진다.

 

 

 

 

화살표(->) 연산자


자바에 람다가 도입 되면서 등장한 연산자로 알고 있다.

람자는 자바8에서 등장 했으며, 기존의 익명 클래스 객체를 만드는 대신 메소드(또는 함수)를 1급 시민으로 값으로 취급하여 전달할 수 있게 하였다.

 

예전에 자바 람다에 대해 정리를 해본 적이 있었다.

지금 생각해보면 람다를 활용하기 전, 처음 공부를 하면서 정리했었기 때문에 다듬어야 할 내용이 많을 것 같다.

이번을 계기로 조만간 다시 한 번 정독하고 내용을 고쳐야겠다.

자바 함수형 인터페이스와 람다식 1편 : 기본 개념과 사용법

자바 함수형 인터페이스와 람다식 2편 : 생략 문법과 제약사항

 

화살표 함수를 요약하자면

왼쪽에 메소드(또는 함수)에서 사용 할 매개변수를 나열한다.

예를 들어

 

public void printParamString(String str) {
    System.out.println("출력할 문자열 : " + str);
}

 

이라는 메소드가 있다고 하면

화살표 왼쪽에 다음과 같이 작성해줄 수 있다.

stringTypeVariable ->

(매개 변수가 하나라면 소괄호를 생략할 수 있다.)

 

그리고 오른쪽에 실제 매개변수를 받아서 처리하는 로직이 작성 된다.

-> {

    System.out.printl("출력할 문자열 : " + stringTypeVariable);

}

 

사용함에 있어서 특별한 조건이 필요하지만

지금은 화살표 연산자 왼쪽엔 매개변수들이, 오른쪽엔 그 매개 값을 사용하는 로직이 작성 된다고 알고 넘어가자.

15주차 람다식에 대해 정리할 때 자세히 다뤄봐야겠다.

 

 

 

3항 연산자


가끔 가독성 문제로 이 연산자를 사용하는 것을 싫어하는 사람들이 있다.

개인적으로 너무 길지 않으면 활용하는 편이다.

 

3항 연산자는 항이 3개라 3항 연산자다.

물음표와 콜론을 사용하며, 기본적인 생김새는 다음과 같다.

 

(조건) ? (조건이 참일 때 실행) : (조건이 거짓일 때 실행)

 

package me.xxxelppa.study.week03;
 
public class Exam_010 {
    public static void main(String[] args) {
        int v1 = 10;
        
        if(v1 > 10) v1 *= 10;
        else v1 -= 5;
    
        System.out.println(v1);
    }
}

 

5

 

담긴 값이 10보다 크면 10배를, 같거나 작으면 -5를 하는 작업을 한다고 했을 때

if조건문을 사용하면 위와 같이 작성할 수 있다.

 

이를 3항 연산자를 사용하면 다음과 같이 작성할 수 있다.

 

package me.xxxelppa.study.week03;
 
public class Exam_011 {
    public static void main(String[] args) {
        int v1 = 11;
        System.out.println(v1 > 10 ? (v1 *= 10) : (v1 -= 5));
    }
}

 

110

 

if 조건문 안에 또 다른 if 조건문을 넣을 수 있는 것처럼, 3항 연산 안에 또 다른 3항 연산을 중첩해서 사용할 수 있다.

하지만 매우 복잡해질 수 있기 때문에 추천하지 않는다.

 

 

 

 

연산자 우선 순위


수학에서도 그렇지만 모든 연산에는 우선순위가 있다.

예를 들어 1 + 2 * 3 을 계산할 때, 곱셈이 덧셈보다 우선순위가 높기 때문에 9가 아닌 7이 연산의 결과가 된다.

 

우선순위 연산자
1 (), []
2 !, ~, ++, --
3 *, /, %
4 +, -
5 <<, >>, >>>
6 <, <=, >, >=
7 ==, !=
8 &
9 ^
10 |
11 &&
12 ||
13 ? :
14 =, +=, -=, *=, /=, <<=, >>=, &=, ^=, ~=

 

자주 사용하는 연산은 굳이 외우지 않아도 익숙해진다. 그리고 사실 전부 외우는 것이 불가능하지 않지만 쉽지도 않다.

연산자의 우선순위 때문에 코딩하는 사람도 읽는 사람도 고통스럽지 않는, 모두가 행복한 방법이 있다.

 

괄호를 적극적으로 사용하면 된다.

 

그렇다고 너무 모든 연산마다 쓸 것 까진 없지만, 애매한 부분에 대해 적절히 사용해주면 가독성 좋은 연산 문장을 작성할 수 있다.

굳이 한 줄에 모든 연산을 표현할 필요는 없으니 (아마도..?) 적절히 여러 라인에 걸쳐 작성해 주는 것도 좋은 방법이라 생각한다.

 

 

 

(optional) Java 13, switch operator


switch 문법은 조건에 따라 분기해야 할 내용이 많아질 경우, 가독성을 포함하여 실행 속도를 향상 시키기 위해 있는 문법으로 알고 있다.

 

이번에 java 13에서 달라진 switch 문법에 대해 알아보다보니 java 12에서도 꽤 많은 변화가 있었던 것 같았다.

그래서 12와 13에서 사용 방법이 어떻게 달라 졌는지 간단한 예제를 작성해 보았다.

 

 

** 아무래도 기분이 개운하지 못해서 조금 더 찾아보고 공부해 보았다.

잘못 전달 될 만한 내용이 있어서 내용을 추가하기로 했다.

 

java 13 에서 switch 는 statement 가 아니고 operator(또는 expression) 라는 것이다.

본문의 제일 처음 정리한 내용을 바탕으로 이해해보면,

연산자는 연산에 사용되는 표시나 기호이다.

그리고 연산은 데이터를 처리하여 결과를 산출해내는 것을 뜻한다.

 

즉, 처리한 결과가 존재한다는 것이다.

그래서 이전의 switch 와 비교했을 때, switch 자체가 연산자로 작동하여 하나의 값으로 취급될 수 있다는 것을 의미한다.

이 사실을 염두에 두고 아래 예제를 보면 조금 더 재미있게 볼 수 있을 것 같다.

 

(참고가 될 만한 링크)

 

package me.xxxelppa.study.week03;
 
public class Exam_012 {
    public static void main(String[] args) {
        /*
         * 가장 기본적인 형태의 switch 문
         */
        System.out.println(switchBasic("a"));   // 1
        System.out.println(switchBasic("e"));   // 3
        
        /*
         * java 12 부터 쉼표(, 콤마)를 사용하여 여러 case 를 한 줄에 나열
         */
        System.out.println(switchWithMultiCase("d"));   // 3
        System.out.println(switchWithMultiCase("f"));   // 3
        
        /*
         * java 12 부터 화살표 (arrow ->) 를 사용하여 결과 반환
         * 더 이상 break 키워드를 사용하지 않아도 원하는 결과를 받아볼 수 있음
         * 실행 결과를 바로 변수에 할당
         */
        System.out.println(switchWithArrow("c"));   // 2
        System.out.println(switchWithArrow("e"));   // 3
        
        /*
         * java 13 부터 yield 키워드를 사용하여 switch 결과 반환
         */
        System.out.println(switchWithJava13Yield("a"));   // 1
        System.out.println(switchWithJava13Yield("e"));   // 3
    }
    
    private static int switchBasic(String str) {
        int result;
        switch (str) {
            case "a":
            case "b":
                result = 1;
                break;
            case "c":
                result = 2;
                break;
            case "d":
            case "e":
            case "f":
                result = 3;
                break;
            default:
                result = -1;
        };
        return result;
    }
    
    private static int switchWithMultiCase(String str) {
        int result;
        switch (str) {
            case "a", "b":
                result = 1;
                break;
            case "c":
                result = 2;
                break;
            case "d", "e", "f":
                result = 3;
                break;
            default:
                result = -1;
        };
        return result;
    }
    
    private static int switchWithArrow(String str) {
        int result = switch (str) {
            case "a", "b" -> 1;
            case "c" -> 2;
            case "d", "e", "f" -> 3;
            default -> -1;
        };
        return result;
    }
    
    private static int switchWithJava13Yield(String str) {
        int result = switch (str) {
            case "a", "b":
                yield 1;
            case "c":
                yield 2;
            case "d", "e", "f" : {
                System.out.println("{} 블록을 사용하여 추가 로직을 수행할 수 있다.");
                yield 3;
            }
            default:
                yield -1;
        };
        return result;
    }
}

 

 

 

 

728x90
728x90

 

# 자바의 프리미티브 타입, 변수 그리고 배열을 사용하는 방법을 익힙니다.


# 학습할 것

  • 프리미티브 타입 종류와 값의 범위 그리고 기본 값
  • 프리미티브 타입과 레퍼런스 타입
  • 리터럴
  • 변수 선언 및 초기화하는 방법
  • 변수의 스코프와 라이프타임
  • 타입 변환, 캐스팅 그리고 타입 프로모션
  • 1차 및 2차 배열 선언하기
  • 타입 추론, var

 

프리미티브 타입 종류와 값의 범위 그리고 기본 값


프리미티브 타입. 영어로 primitive type. 또는 원시 타입 또는 기본형 타입 이라고 하기도 한다.

우선 타입이란 데이터 타입을 줄인 말로 자료형 이라고 하기도 한다.

그럼 데이터 타입 이란 무엇일까.

컴퓨터 관점에서 타입은 데이터가 메모리에 어떻게 저장될 것이고 또 어떻게 다뤄져야 하는지에 대해 알려주는 것이다.

즉, 데이터 타입을 보면 컴퓨터에서 어떤 형태를 가지며 어떻게 처리될 수 있는지 머릿속에 그릴 수 있다.

 

그 중에서 프리미티브 (기본형) 타입에 대해 알아보자.

 

내가 배울 당시 이 기본형을 자바의 8대 자료형 이라고 불렀던 기억이다.

자바 언어에 기본적으로 내장 된 타입으로 표로 정리하면 다음과 같다.

 

구분 프리미티브 타입 메모리 크기 기본 값 표현 범위
논리형 boolean 1 byte false true, false
정수형 byte 1 byte 0 -128 ~ 127
short 2 byte 0 -32,768 ~ 32767
int 4 byte 0 -2,147,483,648 ~ 2,147,483,647
long 8 byte 0L  -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
실수형 float 4 byte 0.0F (3.4 X 10^-38) ~ (3.4 X 10^38) 의 근사값
double 8 byte 0.0  (1.7 X 10^-308) ~ (1.7 X 10^308) 의 근사값
문자형 char 2 byte (유니코드) '\u0000' 0 ~ 65,535

 

이걸 다 알아야 하나?

 

개인적으로 char와 위에서부터 int 까지의 표현범위까지 외우고 있는데, 최소한 메모리 크기를 포함해서 기본 값 까지는 알아야 한다고 생각 한다.

 

기계적으로 외우는게 싫다면 이해를 하면 외우기 쉽다.

1 byte는 8 bit 이다. 그리고 1 bit 는 2진수 한 자리를 뜻한다.

 

우리가 일반적으로 사용하는 10진수는 한 자리에 10가지를 표현할 수 있다. (0 ~ 9)

2진수는 한 자리에 2가지 표현을 할 수 있다. (0 ~ 1)

 

1 비트가 2진수 한 자리를 뜻하면, 2 비트는 2진수 두 자리를 뜻하고 다음과 같은 값을 표현할 수 있다.

00, 01, 10, 11

비트가 1 증가하자 표현할 수 있는 가지수가 2배가 되었다.

 

3 비트로 표현 가능한 값은 다음과 같다.

000, 001, 010, 011, 100, 101, 110, 111

비트가 1 증가하자 표현할 수 있는 가지수가 역시 2배가 되었다.

 

( ** 10진수에서 자릿수가 늘어나면 표현 가능한 수가 10배가 되는 것을 생각하면 된다. ex, 10 -> 100 )

 

여기서 우리는 1 비트가 증가 할 때마다 표현할 수 있는 값이 두 배가 되는 것을 알 수 있다.

확신할 수 없다면 다음과 같이 생각해볼 수 있다.

2 비트로 표현 가능했던 모든 값의 앞에 0을 붙인 것과 1을 붙인 것, 두 그룹으로 만들 수 있다.

** 2 비트로 표현 가능했던 모든 값 : 00, 01, 10, 11

  -> 모든 값 앞에 0을 붙인 값 : 000, 001, 010, 011

  -> 모든 값 앞에 1을 붙인 값 : 100, 101, 110, 111

 

결국 비트가 1 증가 할 경우 표현 가능한 값의 표현 범위가 2배가 된다는 것을 알 수 있다.

그리고 비트의 수와 표현 가능한 값의 수는 2의 거듭제곱으로 나타낼 수 있다는 것도 알 수 있다. (2배씩 커지기 때문에)

 

 

정수형 프리미티브 타입 중 byte 자료형의 메모리 크기는 1 byte이다.

즉 8비트 이다. 8 비트로 표현 가능한 값의 개수는 2의 8제곱 이다.

2의 8 제곱은 256인데 왜 표현범위가 0 ~ 255 가 아니고 -128 ~ 127 까지 일까.

 

컴퓨터에서 음수를 표현하기 위해 MSB 라는 것을 사용한다.

MSB는 Most Significant Bit 의 줄임 말로 최상위 비트를 뜻한다.

최상위 비트란 일반적으로 가장 왼쪽에 위치한 비트를 뜻한다.

 

8비트를 다음과 같이 표현할 수 있다고 가정해보자.

 

x 0 0 0 0 0 0 0

 

x 표시한 가장 왼쪽에 나타낸 비트를 MSB 라고 부르고 부호 비트라고도 한다.

이 값이 1이면 음수, 0이면 양수라고 판단한다.

 

즉, 부호가 있는 자료형의 경우 1비트를 부호를 표현하기 위해 사용하기 때문에

현재 예시를 기준으로 -128 ~ 127 까지의 값 표현 범위를 가진다.

양수는 0이 포함되기 때문에 128이 아니다.

만약 0 ~ 255 까지 표현하고 싶다면, 다시 말해 부호 비트의 자리도 데이터로 취급하려 한다면 unsigned (부호가 없는) 자료형을 사용하면 된다. 음수는 표현하지 못하는 대신 양수 표현 범위가 두 배 늘어난다.

 

아쉽게도 자바에는 unsigned 타입의 자료형을 지원하지 않는다.

그래서 보통 표현 범위를 넘을 때 더 큰 자료형을 사용하고는 한다.

하지만 자바 8 부터 Integer 와 Long 의 wrapper 클래스에 unsigned 관련한 static 메소드가 추가 되었는데, 실제로 활용한 적은 아직 없다.

 

 

이 내용을 이해 했다면 다른 자료형에 대해 몇 바이트의 크기인지 안다면

부호를 가질 때와 가지지 않을 때의 값의 표현 범위를 계산해낼 수 있으므로 굳이 외울 필요가 없어진다.

 

 

실수의 경우 참 재미있다.

분명 정수형과 비교했을 때 메모리 크기는 별반 다르지 않은데, 값의 표현 범위가 훨씬 넓다.

심지어 소수점을 표현할 수 있다니.

 

실수는 부호, 가수(mantissa), 지수(exponent)로 구성되며, 부동 소수점 방식을 사용한다.

부동 소수점 방식을 사용하여 모든 가수를 0 보다 크거나 같고 1보다 작은 값 범위의 값으로 만들고

원래 수를 표현하기 위해 10을 몇 번 거듭 제곱해야 하는지 지수로 표현한다.

 

즉, 1.234 라는 값을 0.1234 * 10^1 로 표현 한다는 것을 의미한다.

 

실수형 중 float 타입은 부호(1비트) + 지수(8비트) + 가수(23비트) = 32비트 를 사용하고

double 타입은 부호(1비트) + 지수(11비트) 가수(52비트) = 64비트 를 사용한다.

 

 

 

다른 얘기지만 그냥 정수형 하나, 실수형 하나, 논리형 하나 이렇게 큼직하고 두루뭉술하게 구분해도 상관 없을 것 같은데 왜 이렇게 잘게(?) 구분해 두었을까?

 

다양한 이유가 있겠지만, 소 잡는 칼로 닭을 잡지 않기 위함 이라고 할 수 있을 것 같다.

야구공 하나 가지고 다니기 위해 야구 가방을 하나 사는게 여행용 캐리어를 사는 것보다 여러가지로 이득이기 때문이다.

 

뭔가 더 정리할 내용이 있는 것 같지만, 주제에 벗어나는 것 같다.

 

 

마지막으로 요즘도 그런지(?) 모르겠지만, 충격적이었던 사실이 있다.

정수형 타입을 선언할 때 byte나 short 타입을 사용하지 말고 int 타입을 사용하라는 것이 기억난다.

왜냐하면 내가 아무리 byte, short 를 사용하겠다고 해도, 지난 시간 알아봤던 JVM이 내부에서 전부 4 바이트 크기로 만들어 관리한다고 알고 있다.

즉, 4바이트보다 작은 타입에 대해, 내가 크기에 맞게 사용하겠다고 아무리 byte, short 해도 JVM이 무조건 기본적으로 4바이트 크기 타입으로 생각하기 때문에 두 번 일하게 하지 말라는 얘기를 들은 기억이 있다.

 

물론 배열에서 사용 할 때는 또 다르지만.. 주제를 너무 벗어나는 것 같다.

 

 

 

프리미티브 타입과 레퍼런스 타입


프리미티브 타입에 대해서는 위에서 살펴 보았다.

레퍼런스 타입이란 무엇일까.

 

reference. 참고, 참조의 뜻을 가지고 있다. 그래서 참조 타입이라고 하는 사람도 많이 있다.

 

참조 한다는 것을 무엇일까.

여러가지 의미로 해석할 수 있겠지만, 자바 언어에서는 실제 값이 저장되어 있는 곳의 위치를 저장한 값(주소값)을 뜻한다.

참조 타입의 종류는 배열, 열거(enum), 클래스 그리고 인터페이스가 있다.

 

기본 타입과 참조 타입을 구분하는 방법은 생각보다 단순하다.

저장되는 값이 실제 값 그 자체이냐 아니면 메모리의 주소값이냐에 따라 구분할 수 있다.

 

그럼 이 값은 어디에 저장 되는 걸까.

지난 1주차에 공부한 JVM의 Runtime Data Area 이다.

그 중에서도 런타임 스택 영역과 가비지 컬렉션 힙 영역에 저장 된다.

 

예륻 들어 다음과 같은 코드가 있다고 생각하자.

 

package me.xxxelppa.study.week02;
 
public class Exam_001 {
    public static void main(String[] args) {
        String name = "xxxelppa";
        int age = 17;
    }
}

 

레퍼런스 타입의 name 변수와 프리미티브 타입의 age 변수는 런타임 스택 영역에 생성 된다.

그리고 레퍼런스 타입의 값인 주소값과, 프리미티브 타입의 값인 17 역시 런타임 스택 영역에 저장 된다.

 

다만, 레퍼런스 타입의 값인 주소값이 가리키는 실제 값은 가비지 컬렉션 힙 영역에 객체가 생성 된다.

그래서 값을 복사할 때 조심해야 한다.

 

그 이유는 프리미티브 타입의 경우 실제 값이 아닌 주소값이 복사되기 때문이다.

보통 기본서에서는 값에 의한 복사 (call by value)와 참조 또는 주소에 의한 복사 (call by reference) 라고 한다.

값에 의한 복사가 아닌 경우 두가지 경우가 있는데 얕은 복사와 깊은 복사로 또 나뉜다.

 

얕은 복사는 주소값을 복사하여 결국 동일한 가비지 컬렉션 힙 영역의 객체를 참조한다.

그래서 이런 복사를 의도하지 않았을 경우 치명적인 오류가 발생할 수 있다.

 

깊은 복사는 프리미티브 타입에서의 값에 의한 복사처럼 완전히 똑같은 새로운 객체를 만들어 복사하는 것을 뜻한다.

 

어느 방식이 좋다 나쁘다는 없고, 의도한 상황에 맞게 적절히 잘 판단하여 사용해야 한다.

 

 

728x90

 

 

 

리터럴


요약하자면 리터럴은 실제로 저장되는 값 그 자체로 메모리에 저장되어있는 변하지 않는 값 그 자체를 뜻한다.

또는 컴파일 타임에 프로그램 안에 정의되어 그 자체로 해석 되어야 하는 값을 뜻한다.

어떻게 표현해도 말이 어려운것 같다. 그냥 코드 내에서 직접 쓴 값이라고 생각하는게 편할것 같다.

 

그 종류로는 정수, 실수, 문자, 부울(논리), 문자열 등이 있다.

어디서 본 것 같다면 문자열을 제외하고 프리미티브 타입으로 표현 가능하다는 것을 알 수 있다.

 

각 프리미티브 타입에 대한 설명은 앞서 했으므로 여기서는 생략하고

대신 예제 코드를 하나 작성 해보기로 했다.

 

package me.xxxelppa.study.week02;
 
public class Exam_002 {
    public static void main(String[] args) {
        System.out.println("===== 정수 리터럴 =====");
        int int_v1 = 0b10;    // 접두문자 0b   -> 2진수
        int int_v2 = 010;     // 접두문자 0    -> 8진수
        int int_v3 = 10;      // 접두문자 없음 -> 10진수
        int int_v4 = 0x10;    // 접두문자 0x   -> 16진수
        long long_v1 = 10L;   // 접미문자 l 또는 L -> long 타입 리터럴
 
        System.out.println("2진수 정수 리터럴 : " + int_v1);
        System.out.println("8진수 정수 리터럴 : " + int_v2);
        System.out.println("10진수 정수 리터럴 : " + int_v3);
        System.out.println("16진수 정수 리터럴 : " + int_v4);
        System.out.println("long 타입 정수 리터럴 : " + long_v1);
        System.out.println();
 
        System.out.println("===== 실수 리터럴 =====");
        // 실수 타입 리터럴은 double 타입으로 컴파일 되므로
        // float 타입인 경우 명시적으로 f 또는 F 를 명시해줘야 한다.
        // double 타입도 d나 D를 명시해줘도 되지만, 안해줘도 상관 없다.
        float float_v1 = 1.234F;
        double double_v1 = 1.234;
        double double_v2 = 1.234d;
        double double_v3 = 1234E-3d;
 
        System.out.println("float 타입 실수 리터럴 : " + float_v1);
        System.out.println("double 타입 실수 리터럴 1 : " + double_v1);
        System.out.println("double 타입 실수 리터럴 2 : " + double_v2);
        System.out.println("double 타입 실수 리터럴 3 : " + double_v3);
        System.out.println();
 
 
        System.out.println("===== 문자 리터럴 =====");
        char char_v1 = 'C';
        char char_v2 = '민';
        char char_v3 = '\u1234';    // 백슬러시 u 다음 4자리 16진수 유니코드
 
        System.out.println("문자 리터럴 1 : " + char_v1);
        System.out.println("문자 리터럴 2 : " + char_v2);
        System.out.println("문자 리터럴 3 : " + char_v3);
        System.out.println();
 
 
        System.out.println("===== 부울(논리) 리터럴 =====");
        boolean boolean_v1 = true;
        boolean boolean_v2 = 12 > 34;
 
        System.out.println("부울(논리) 리터럴 1 : " + boolean_v1);
        System.out.println("부울(논리) 리터럴 2 : " + boolean_v2);
        System.out.println();
 
 
        System.out.println("===== 문자열 리터럴 =====");
        String string_v1 = "hello, ws study";
        System.out.println("문자열 리터럴 : " + string_v1);
        System.out.println();
    }
}

 

===== 정수 리터럴 =====
2진수 정수 리터럴 : 2
8진수 정수 리터럴 : 8
10진수 정수 리터럴 : 10
16진수 정수 리터럴 : 16
long 타입 정수 리터럴 : 10

===== 실수 리터럴 =====
float 타입 실수 리터럴 : 1.234
double 타입 실수 리터럴 1 : 1.234
double 타입 실수 리터럴 2 : 1.234
double 타입 실수 리터럴 3 : 1.234

===== 문자 리터럴 =====
문자 리터럴 1 : C
문자 리터럴 2 : 민
문자 리터럴 3 : ሴ

===== 부울(논리) 리터럴 =====
부울(논리) 리터럴 1 : true
부울(논리) 리터럴 2 : false

===== 문자열 리터럴 =====
문자열 리터럴 : hello, ws study

 

대입 연산자를 기준으로 모든 우항의 값들을 리터럴 이라고 부른다.

 

 

 

 

변수 선언 및 초기화하는 방법


자바에서 변수를 선언하는 방법은 기본적으로 그 변수의 타입(자료형) 다음에 변수의 이름을 작성하는 것으로 한다.

예를 들어 정수형 타입의 변수를 다음과 같이 선언할 수 있다.

 

package me.xxxelppa.study.week02;
 
public class Exam_003 {
    public static void main(String[] args) {
        int value1;     // 정수형 타입의 변수 value1 을 선언
    }
}

 

한 번에 여러개의 변수를 선언한다면 다음과 같이도 할 수 있다.

 

package me.xxxelppa.study.week02;
 
public class Exam_004 {
    public static void main(String[] args) {
        // 한 번에 여러개의 정수형 타입 변수를 선언
        int value1, value2, value3;
    }
}

 

초기화 하는 방법은 대입 연산자인 등호를 사용한다.

처음 등호를 접하면 좌항과 우항의 값이 동등하다는 것을 뜻할 때 쓰이는 것이 익숙하겠지만

프로그래밍에서 등호는 우항의 값을 좌항의 변수에 할당 한다는 의미로 쓰인다.

이름도 대입 연산자라고 부른다.

 

동등하다는 것을 나타내고 싶을 때는 등호를 두 번 사용한다. (==)

 

초기화 한다는 것은, 선언한 변수에 실제 값을 넣는다는 것을 의미한다.

위에서 선언한 변수에 값을 초기화 해보자.

 

package me.xxxelppa.study.week02;
 
public class Exam_005 {
    public static void main(String[] args) {
        // 1. 선언과 동시에 초기화
        int value1 = 10;
 
        // 2. 선언한 다음 초기화
        int value2;
        value2 = 20;
    }
}

 

갑자기 호기심이 생겨 바로 위의 코드를 컴파일 한 class 파일을 IDE를 사용해서 열어 보았다.

 

// Compiled from WS_live_study.java (version 1.8 : 52.0, super bit)
public class Day02.WS_live_study {
  
  // Method descriptor #6 ()V
  // Stack: 1, Locals: 1
  public WS_live_study();
    0  aload_0 [this]
    1  invokespecial java.lang.Object() [8]
    4  return
      Line numbers:
        [pc: 0, line: 3]
      Local variable table:
        [pc: 0, pc: 5] local: this index: 0 type: Day02.WS_live_study
  
  // Method descriptor #15 ([Ljava/lang/String;)V
  // Stack: 1, Locals: 3
  public static void main(java.lang.String[] ar);
    0  bipush 10
    2  istore_1 [value1]
    3  bipush 20
    5  istore_2 [value2]
    6  return
      Line numbers:
        [pc: 0, line: 6]
        [pc: 3, line: 10]
        [pc: 6, line: 11]
      Local variable table:
        [pc: 0, pc: 7] local: ar index: 0 type: java.lang.String[]
        [pc: 3, pc: 7] local: value1 index: 1 type: int
        [pc: 6, pc: 7] local: value2 index: 2 type: int
}

 

너무 단순한 코드여서 그런 것인지 모르겠지만, 선언과 동시에 하는 방법과 따로 하는 방법에 별다른 차이는 없어 보인다.

 

 

 

변수의 스코프와 라이프타임


변수의 스코프는 그 변수에 접근할 수 있는 범위 라고 생각하는게 무난할 것 같다.

자바 언어는 블록 스코프를 사용 한다. (블록은 중괄호 {} 를 뜻한다.)

 

package me.xxxelppa.study.week02;
 
public class Exam_006 {
    // 여기 선언된 변수는 Exam_006 {} 블록 내에서 접근 가능하다.
    static int myBlock = 10;
    public static void main(String[] args) {
        System.out.println("result : " + myBlock);
    }
}

 

5라인에 선언한 myBlock 변수는 3라인의 WS_live_study {} 블록 내에서 접근 가능하기 때문에

이 코드를 실행하면 다음과 같은 결과를 볼 수 있다.

 

result : 10

 

그럼 다음과 같은 경우는 어떤 결과가 나올까?

 

package me.xxxelppa.study.week02;
 
public class Exam_007 {
    // 여기 선언된 변수는 Exam_007 {} 블록 내에서 접근 가능하다.
    static int myBlock = 10;
 
    public static void main(String[] args) {
        int myBlock = 20;
        System.out.println("result : " + myBlock);
    }
}

 

예상 했겠지만 20을 출력하는 것을 확인할 수 있다.

 

result : 20

 

9라인에서 myBlock을 사용할 때, 이 값을 자신과 가까운 블록 스코프에서 찾고

없을 경우 상위 블록 스코프에 존재하는지 찾아본다.

 

레퍼런스 타입의 변수의 라이프 타임은 쓰레기 수집기 (GC : Garbage Collector)와 관련이 있다.

이 GC는 가비지 컬렉션 힙 영역에 존재하는 참조 타입 변수의 객체에 대해 동작한다.

힙 영역에 메모리가 부족할 경우 GC가 이 영역을 스캔하고, 아무곳에서도 사용하지 않는 즉, 참조 되고 있지 않은 객체를 제거해 버린다.

예를 들면 다음과 같은 경우다.

 

package me.xxxelppa.study.week02;
 
public class Exam_008 {
    public static void main(String[] args) {
        MyTest mt = new MyTest();
        mt = null;
    }
}
 
class MyTest {}

 

5라인에서 MyTest 클래스의 객체를 생성해서 mt 변수에 할당 했다.

여기까지 하면, 런타임 스택 영역에 mt 변수가 생성되고, 그 값은 가비지 컬렉션 힙 영역에 생성 된 new MyTest() 로 만들어진 객체가 저장된 주소값을 가지고 있다.

 

이때 런타임 스택 영역의 mt 변수의 값인 주소값에 null을 할당하면, new MyTest() 로 만든 이 객체는 더이상 아무도 참조하지 않게 된다.

이런 객체가 GC의 대상이 된다.

 

마지막으로 런타임 스택 영역에 생성된 변수의 라이프 타임은 블록 스코프에 의존적이다.

즉, 블록 내에서 선언된 변수는 블록이 종료될 때 런타인 스택 영역에서 함께 소멸한다.

 

 

 

타입 변환, 캐스팅 그리고 타입 프로모션


앞서 타입이란 데이터 타입을 줄인 말이고, 다른 말로 자료형 이라고도 한다고 했다.

특정 데이터 타입으로 표현된 리터럴은 다른 데이터 타입으로 변환할 수 있다.

예를 들어 int 타입 변수에 담긴 값을 long 타입 변수에 담을 수 있다.

package me.xxxelppa.study.week02;
 
public class Exam_009 {
    public static void main(String[] args) {
        int v1 = 100;
        long v2 = v1;
 
        System.out.println("v1 : " + v1);
        System.out.println("v2 : " + v2);
    }
}

실행 결과 모두 100의 값을 출력 한다.

 

이렇게 변환 될 때 크게 두가지 경우를 생각해볼 수 있다.

 

1. 자신의 표현 범위를 모두 포함한 데이터 타입으로의 변환. (타입 프로모션)

2. 자신의 표현 범위를 모두 포함하지 못한 데이터 타입으로의 변환. (타입 캐스팅)

 

조금 복잡해 보이지만 이렇게 설명을 남기는 이유가 있다.

간혹 표현 범위의 크기를 가지고 분류하는 사람들이 있었기 때문이다.

 

예를 들어 실수형 데이터 타입인 float의 경우 메모리 크기가 4 byte 이고

정수형 데이터 타입인 long의 경우 메모리 크기가 8 byte 이다.

만약 표현 범위의 크기만 가지고 본다면 float 데이터 타입의 값을 long 타입으로 변환한다고 가정하자.

4 byte 메모리 크기를 갖는 값을 8 byte 메모리 크기의 데이터 타입으로 변환하기 때문에 타입 프로모션이라 생각하는 사람을 만난적이 있다.

 

타입 프로모션과 타입 캐스팅을 구분하기 위해서는 메모리 크기가 아닌 데이터 표현 범위를 따져봐야 한다.

 

지금 예시를 생각해보면, 실수를 표현하는 float 데이터 타입의 값을 정수를 표현하는 long 데이터 타입 값으로 변환을 시도한다면,

long 데이터 타입은 실수를 표현할 수 없기 때문에 원본 데이터에 손실이 발생 할 수 있다.

 

이렇게 원본 데이터가 담긴 데이터 타입의 표현 범위를 변환 할 데이터 타입의 표현 범위가 모두 수용하지 못할 경우 데이터 손실이 발생할 수 있는데, 이것을 타입 캐스팅 이라고 한다.

반대로 모두 수용할 수 있다면 타입 프로모션 이라고 한다.

 

말보단 코드를 보는 것이 좋을 것 같다. 위 상황을 구현해보았다.

 

똑똑한 IDE는 위와 같이 타입 캐스팅이 발생할 경우 컴파일 타임에 오류를 발생한다.

메시지는 다음과 같다.

 

Type mismatch: cannot convert from float to long

 

그리고 어떻게 수정해야할지 추천도 해준다.

 

 

Add cast to 'long' 을 선택하면 코드가 다음과 같이 자동으로 바뀌는 것을 볼 수 있다.

 

package me.xxxelppa.study.week02;
 
public class Exam_010 {
    public static void main(String[] args) {
        float float_v1 = 1.23f;
//        long long_v1 = float_v1;
        long long_v1 = (long)float_v1;
 
        System.out.println("float_v1 : " + float_v1);
        System.out.println("long_v1 : " + long_v1);
    }
}

 

그럼 이 코드를 실행해보자.

 

float_v1 : 1.23
long_v1 : 1

 

소수점을 표현할 수 있는 실수 데이터 타입을 정수 데이터 타입으로 강제로 변환 했기 때문에 원본 데이터가 온전히 변환되지 않았다.

물론 실수 데이터 타입에 담은 리터럴의 소숫점 아래 값이 없었다면 온전히 정수로 표현이 됐을 것이다.

그렇기 때문에 타입 캐스팅을 할 경우 원본 데이터에 손실이 발생 할 가능성이 있다고 한다. (무조건 손실이 일어나지 않기 때문에)

 

 

타입 프로모션의 경우 타입 캐스팅과 같이 어떤 데이터 타입으로 변환해야 하는지 명시하지 않아도 된다.

 

다음 코드는 float -> long 으로 타입 캐스팅 했던 것을 long -> float 로 타입 프로모션을 하도록 고친 것이다.

 

package me.xxxelppa.study.week02;
 
public class Exam_011 {
    public static void main(String[] args) {
        long long_v1 = 123L;
        float float_v1 = long_v1;
 
        System.out.println("long_v1 : " + long_v1);
        System.out.println("float_v1 : " + float_v1);
    }
}

 

long_v1 : 123
float_v1 : 123.0

 


데이터 타입을 변환할 경우를 자주 볼 수 있는데, 타입 캐스팅을 할 경우 앞서 말했지만 원본 데이터가 손실 될 수 있기 때문에 조심해서 다뤄야 한다.

 

 

 

 

1차 및 2차 배열 선언하기


1차, 2차 배열을 선언하기에 앞서 배열이 무엇인지 먼저 알아야 한다.

배열은 동일한 자료형을 정해진 수만큼 저장하는 순서를 가진 레퍼런스 타입 자료형이다.

이런 것을 왜 만 들었을까.

 

다음은 배열을 설명할 때 자주 등장하는 상황이다.

숫자 수집을 좋아하던 xxxelppa는 길을 가다 3개의 숫자를 발견하게 되어 이를 컴퓨터에 저장해두기로 했다.

 

package Day02;
 
public class WS_live_study {
    public static void main(String[] ar) {
        int num_1 = 10;
        int num_2 = 20;
        int num_3 = 30;
        
        System.out.println("1 번째 수집한 수 : " + num_1);
        System.out.println("2 번째 수집한 수 : " + num_2);
        System.out.println("3 번째 수집한 수 : " + num_3);
    }
}

 

1 번째 수집한 수 : 10
2 번째 수집한 수 : 20
3 번째 수집한 수 : 30

 

다음날 길에서 새로운 2개의 숫자를 발견해서 같은 방법으로 컴퓨터에 저장 했다.

 

package me.xxxelppa.study.week02;
 
public class Exam_012 {
    public static void main(String[] args) {
        int num_1 = 10;
        int num_2 = 20;
        int num_3 = 30;
        int num_4 = 40;
        int num_5 = 50;
 
        System.out.println("1 번째 수집한 수 : " + num_1);
        System.out.println("2 번째 수집한 수 : " + num_2);
        System.out.println("3 번째 수집한 수 : " + num_3);
        System.out.println("4 번째 수집한 수 : " + num_4);
        System.out.println("5 번째 수집한 수 : " + num_5);
    }
}

 

1 번째 수집한 수 : 10
2 번째 수집한 수 : 20
3 번째 수집한 수 : 30
4 번째 수집한 수 : 40
5 번째 수집한 수 : 50

 

이렇게 매일 매일 평화롭게 숫자를 수집하고 있었는데, 어느 날 갑자기 100개의 숫자를 한번에 발견하게 되었다.

수집한 숫자가 많아질 수록 int num_{숫자} 를 계속 복사 붙여넣기 하고 출력을 할 때도 매번 똑같은 짓을 반복하는 자신을 발견했다.

 

심지어 얼마 전 초고층 빌딩 유리창 청소 아르바이트가 끝나 IDE를 살 수 없었던 xxxelppa는 절망했고,

복사 붙여넣기를 잘못 한 탓인지 중복된 변수명을 사용하거나 같은 숫자를 두 번 이상 출력하는 등 엉망진창이 되어버렸다.

 

더 이상 숫자 수집을 할 수 없다는 슬픔에 울다 지쳐 잠이 들었는데, 자바신이 whiteship을 타고 나타나 배열을 사용하라는 말을 남기고 떠나는 꿈을 꾸었다.

 

다음은 배열을 사용해서 숫자를 수집하고 출력한 예제이다.

 

package me.xxxelppa.study.week02;
 
public class Exam_013 {
    public static void main(String[] args) {
        int[] collect_num = new int[5];
 
        collect_num[0] = 10;
        collect_num[1] = 20;
        collect_num[2] = 30;
        collect_num[3] = 40;
        collect_num[4] = 50;
 
        for (int i = 0; i < 5; ++i) {
            System.out.println((i+1) + " 번째 수집한 수 : " + collect_num[i]);
        }
    }
}

 

사실 아직 반복문에 대해 알아보지 않았지만, 더이상 변수 이름을 계속 지을 필요도 없어졌고

내용을 출력하는 것도 보다 간결하게 구현할 수 있게 되었다. (배열은 4주차 '제어문' 에서 자세히 볼 반복문과 함께 자주 사용 된다.)

 

여기서 눈 여겨 볼 것은

1. 동일한 데이터 타입을 하나의 배열로 관리할 수 있다는 것과

2. 배열은 순서를 가지고 있는데, 1부터 시작하지 않고 0부터 시작한다는 것이다. (간혹 zero base 라고도 표현 한다.)

 

 

다음으로 배열을 선언하는 방법에 대해 알아보자.

배열 타입은 대괄호 [] 를 사용하고, 크게 두가지 방법으로 선언할 수 있다.

 

package me.xxxelppa.study.week02;
 
public class Exam_014 {
    public static void main(String[] args) {
        int[] type_1;
        int type_2[];
    }
}

 

이렇게 선언한 배열 변수에 값을 할당하는 방법은 다음과 같다.

 

package me.xxxelppa.study.week02;
 
public class Exam_015 {
    public static void main(String[] args) {
        int[] type_1 = new int[5];
        int[] type_2 = {10, 20, 30, 40, 50};
        int[] type_3 = new int[]{10, 20, 30, 40, 50};
 
        // Array constants can only be used in initializers
//        type_2 = {10, 20, 30, 40, 50};
    }
}

 

직접 new 연산자를 사용해서 배열 객체를 생성하는 방법과.

어떤 값을 할당할지 정해진 경우 중괄호를 사용해서 간단하게 배열 객체를 만드는 방법이 있다.

 

6라인의 배열 객체 생성 및 할당 방법은 변수 선언과 동시에 할당할 경우에만 사용할 수 있는 방법이다.

즉, 다음과 같은 것은 컴파일 오류가 발생한다.

 

 

선언한 배열 변수는 JVM의 런타임 스택 영역에 생성 된다.

그리고 배열은 레퍼런스 타입이기 때문에 값은 가비지 컬렉션 힙 영역에 객체가 생성 된다.

이 힙 영역의 주소 값이 런타입 스택 영역에 생성된 변수의 값으로 할당 된다.

 

즉, 다음과 같다.

 

package me.xxxelppa.study.week02;
 
public class Exam_016 {
    public static void main(String[] args) {
        int[] type_1 = new int[5];
    }
}

 

 

 

100번지는 예시로 쓴 주소 값으로 실제와는 많이 다르다.

자바는 사용자(개발자)가 직접 메모리에 접근하지 않고 할 수도 없다.

 

배열의 [0], [1], ... [n] 을 각각 배열의 요소 또는 원소 라고 부른다.

각 원소는 배열의 타입 크기를 갖는다.

길이 5인 배열을 생성하면 다음과 같다.

 

 

그리고 가비지 컬렉션 힙 영영에 생성되는 데이터는 각 타입의 기본값으로 초기화 된다.

** 각 원소의 크기가 4 byte인 것은 예시를 int 데이터 타입으로 만들었기 때문이다. )

 

2차원 배열 부터는 다차원 배열에 속한다.

살면서 알고리즘 문제에서 의도적으로 3차원 배열을 사용한 문제를 풀어본 기억 외에

2차원보다 고차원의 배열을 사용해본 기억이 거의 없을 정도로 2차원 배열까지만 자주 사용한다.

 

수학에서 얘기하는 점, 선, 면을 떠올리면 배열을 머릿속으로 그려볼 수 있다.

1차원 배열은 선을, 2차원 배열은 면을 떠올리면 된다.

특히 2차원 배열의 경우 행렬 형태로 자주 표현한다.

 

혹시나 3차원 배열이 궁금하다면, 큐브 형태를 떠올리면 된다.

 

2차원 배열의 선언은 다음과 같이 할 수 있다.

 

package me.xxxelppa.study.week02;
 
public class Exam_017 {
    public static void main(String[] args) {
        int[][] type_1;
        int type_2[][];
    }
}

 

여기서 대괄호를 한번 더 써주면 3차원 배열이 된다.

값을 할당하는 방법은 다음과 같이 할 수 있다.

 

package me.xxxelppa.study.week02;
 
public class Exam_018 {
    public static void main(String[] args) {
        int[][] type_1 = new int[2][3];
        int[][] type_2 = {{1, 2}, {3, 4, 5}};
        int[][] type_3 = new int[][]{{1, 2}, {3, 4, 5}};
    }
}

 

예제 코드의 5라인 new int[2][3] 배열을 기준으로, 2차원 배열은 메모리상에 다음과 같이 생성 된다.

 

 

 

예제코드 6라인과 7라인의 경우 위 그림과는 조금 다르다.

 

 

 

이것을 행렬로 표현한다면 다음과 같다.

 

1 2  
3 4 5

 

테이블 표현상 첫번째 행의 2 오른쪽에 공간이 있는것 처럼 보이지만

예제 코드 기준으로 존재하지 않는 공간이다.

 

이렇게 각 행에 대해 열의 길이가 다른 배열을 지그재그하다 해서 재기드 배열(jagged array) 이라 하기도 한다.

 

 

 

 

타입 추론, var


타입 추론(Type inference) 이란 값을 보고 컴파일러가 데이터 타입이 무엇인지 추론 한다는 것을 의미한다.

javascript를 예로 들면, 모든 변수를 var, let, const 등을 사용해서 선언한다.

자바에서처럼 int, long, boolean 등의 데이터 타입을 명시하지 않고 사용한다.

 

타입 추론에 대해선 대표적으로 제네릭에서 볼 수 있다.

예를 들면 다음과 같다.

 

package me.xxxelppa.study.week02;
 
import java.util.HashMap;
 
public class Exam_019 {
    public static void main(String[] args) {
        HashMap<String, Integer> myHashMap = new HashMap<>();
    }
}

 

7라인에서 myHashMap에 HashMap 객체를 할당할 때 new HashMap<String, Integer>() 를 사용하지 않고, new HashMap<>() 을 사용했다.

이것은 myHashMap 변수에 담길 데이터 타입이 HashMap<String, Integer> 라는 것을 myHashMap 변수의 데이터 타입을 바탕으로 추론해낼 수 있기 떄문이다.

 

타입 추론에 대해서는 이번에 처음 접하게 되었는데, 이런 타입 추론이 조금 더 확장 된 것 같다.

개인적으로 보다 명시적인 코딩을 지향하기 때문에 제네릭이나 람다에서 사용하는 것 이상의 타입 추론을 자주 사용하지 않을것 같다.

나중에 생각이 바뀐다면 또 모르겠지만 지금은 그렇다.

 

var를 사용할 경우 제약 사항이 몇가지 존재한다.

1. 로컬 변수이면서

2. 선언과 동시에 값이 할당 되어야 한다는 것이다.

 

 

로컬 변수로 선언하지 않아서 발생한 오류이다.

 

 

선언과 동시에 값을 할당하지 않아서 발생한 오류이다.

 

 

다른 언어에 비해 타입 추론 개념이 늦게 도입된 이유는 자바 개발진들이 매우 보수적이며, 하위 호환성을 매우 중요하게 생각하기 때문이라고 한다.

 

자바에서 var 를 보다 활용도 있게 사용할 수 있는 방법에 대해 정리해둔 글을 보아서 링크를 추가했다.

 

Java 10 에서 var 재대로 사용하기 1

Java 10 에서 var 제대로 사용하기 2

 

 

 

 

 

728x90
728x90

# 자바 소스 파일 (.java)을 JVM 으로 실행하는 과정 이해하기.


# 학습할 것

  • JVM 이란 무엇인가
  • 컴파일 하는 방법
  • 실행하는 방법
  • 바이트코드란 무엇인가
  • JIT 컴파일러란 무엇이며 어떻게 동작하는지
  • JVM 구성 요소
  • JDK 와 JRE의 차이

JVM 이란 무엇인가


JVM -> Java Virtual Machine '자바 가상 머신' 을 뜻하는 말로 바이트코드를 실행하는 주체이다.

자바가 처음 세상에 나왔을 때 WORA (Write Once Run Anywhere) 를 내세워 홍보한 것으로 알고있다.

한번 작성해서 어디서든 실행할 수 있다는 말로, 자바 코드로 작성한 프로그램은 실행할 환경 (예를 들면 운영체제)에 독립적으로 실행할 수 있음을 뜻한다.

 

내가 아는 지식으로는 하드웨어 위에 운영체제를 설치하고, 각 운영체제에 맞는 JVM을 설치한다.

 

예전에 설명하기 위해 사용했던 그림이다.

 

 

JVM과 같은 중간언어를 해석해주는 추상화된 장치가 없는 언어들은, 운영체제가 바뀔 때마다 그에 맞는 실행 가능한 프로그램을 만들어야 했다. 이런 불편함을 해소하기 위해 바이트 코드를 기계어로 번역해주는 과정을 한 번 더 하며 성능을 포기하고 편리함을 선택했다.

그래서 처음 자바를 배울 때 다른 언어에 비해 실행 속도가 느리다는 말을 많이 들었다.

왜냐하면 직접 실행 가능한 코드가 아닌 바이트코드를 만들고, 이 바이트 코드를 JVM이 인터프리터 방식으로 기계어로 번역하며 실행하기 때문이다.

 

 

 

728x90

 

컴파일 하는 방법


컴파일을 한다는 것은 .java 파일을 .class 파일(바이트코드)을 만든다는 것을 의미 한다.

JDK (Java Development Kit) 자바 개발 도구를 설치하면 bin 폴더 안에 javac 라는 java compiler 가 포함되어 있다.

이 명령을 사용해서 .class 파일을 만든다.

 

 

명령프롬프트를 사용해서 컴파일 하기 위해서는 현재 위치를 소스파일이 있는 곳으로 이동해야 한다.

나는 C드라이브의 dev 폴더 아래 WS_live_study 라는 폴더를 만들었고

그 안에 WS_live_study.java 라는 파일을 만들었다.

 

그리고나서 javac WS_live_study.java 라고 명령을 하면, 작성한 코드에 문제가 없을 경우 .class 파일의 바이트코드가 생성 된다.

 

여기서 한 번 생각해 봐야 할 것이 있다.

자연스럽게 javac 명령어를 사용했는데, 어떻게 가능했을까.


환경변수라는 것에 대해서 알 필요가 있다.

환경변수는 운영체제가 실행할 수 있는 실행파일들이 위치한 디렉토리를 지정해두어 이를 운영체제가 참조해서 사용할 수 있도록 한 것이다.

 

 

두 번째 방법은 default 패키지를 사용하지 않았을 때 방법이다.

IDE를 사용하면서 자꾸 까먹는 방법인것 같다.

 

 

 

실행하는 방법


 

실행할때는 java 명령어를 사용한다.

 

 

 

두 번째 방법은 default 패키지를 사용하지 않았을 때 방법이다.

이것 역시 IDE 를 사용하면서 자꾸 까먹게 되는 방법인것 같다.

 

 

 

바이트코드란 무엇인가


프로그램을 실행하는 것은 결국 컴퓨터이다. 다시 말해 프로그램은 컴퓨터가 이해할 수 있는 형태로 작성되어 있어야 한다.

자바 문법으로 작성한 .java 파일은 사람이 이해할 수 있는 언어로 작성했기 때문에 컴퓨터는 이해할 수 없다.

그렇기 때문에 번역을 통해 컴퓨터가 이해할 수 있는 형태로 만들어 줘야 한다.

 

컴퓨터가 이해할 수 있는 형태로 번역하는 것은 JVM이 담당한다.

그럼 우리는 JVM이 이해할 수 있는 형태로 번역을 해서 전해줘야 한다.

이때 이 JVM이 이해할 수 있는 형태가 바이트코드이다.

 

자바에서 javac 명령을 통해 컴파일을 하면 .class 확장자를 갖는 바이트코드가 만들어지고, JVM이 바이트코드를 실행한다.

 

일반적으로 사용할때는 특정한 플랫폼이 아닌 플랫폼 위에 설치된 JVM과 같은 머신 위에서 실행 가능한 코드라고 알고있다.

어디선가 이런것을 중간언어라고 부르는걸 본 기억이 있다.

 

 

 

JIT 컴파일러란 무엇이며 어떻게 동작하는지


JIT 컴파일러는 Just In Time 컴파일러로 바이트코드를 기계어로 번역하여 실행하는 것을 뜻한다.

인터프리터 방식은 바이트코드를 한 줄씩 읽으면서 (번역하면서) 코드를 실행하기 때문에 동일한 메소드를 실행하는 경우 중복해서 번역하는 비효율이 있다.

이를 방지해서 보다 좋은 성능을 낼 수 있게 하기 위해 JIT 컴파일러는 번역한 내용을 캐싱해 두었다가 동일한 메소드를 실행할 경우 다시 번역하지 않고 캐싱된 내용을 실행하는 것으로 알고 있다.

 

 

[검색한 내용]

 

JIT 컴파일은 다른 말로 동적 번역 이라고 한다.

이것은 프로그램을 실제로 실행하는 시점에 기계어로 번역하는 컴파일 기법이다.

 

전통적으로 컴퓨터 프로그램을 만드는 방법은 두가지가 있는데

1. 인터프리트 방식과

2. 정적 컴파일 방식으로 나눌 수 있다.

 

인터프리트 방식은 실행 중 프로그래밍 언어를 읽으며 해당 기능에 대응하는 기계어 코드를 실행한다.

정적 컴파일은 실행하기 전에 프로그램 코드를 기계어로 번역한다.

 

JIT 컴파일러는 두 가지 방식을 혼합한 방식으로 생각할 수 있다.

실행 시점에 인터프리트 방식으로 기계어 코드를 생성 하면서 그 코드를 캐싱한다.

그리고 같은 함수가 여러 번 불릴 때 매번 기계어 코드를 생성하는 것을 방지한다.

 

최근 JVM과 .NET V8 (node.js) 에서 JIT 컴파일러를 지원한다.

 

 

 

JVM 구성 요소


JVM은 크게 네가지 구성 요소를 가진다.

1. Class Loader

2. GC (Garbage Collector)

3. Execution Engine

4. Runtime Data Area

 

 

1. Class Loader

 :: JRE의 일부로, 바이트코드를 실행할 때 class 객체를 메모리에 생성하는 요소이다.

    클래스의 인스턴스를 생성하면 Class Loader를 통해 메모리에 로드한다.

 

2. GC (Garbage Collector)

 :: 자바는 메모리 관리를 사용자가 아닌 JVM이 알아서 해준다.

    GC는 더이상 참조되지 않는 메모리를 정리해준다.

    GC가 언제 호출되는지는 알 수 없으며, 심지어 사용자가 호출하더라도 메모리 정리할 필요가 없다고 판단하면 실행하지 않는다.

 

3. Execution Engine

 :: 메모리에 로드 된 바이트코드를 실행하는 역할을 한다.

    Class Loader를 통해 Runtime Data Area에 배치된 바이트코드는 Execution Engine에 의해 실행 된다.

    인터프리터 방식이나 JIT 방식으로 실행한다.

 

4. Runtime Data Area

 :: JVM의 메모리 영역이다.

    크게 네가지 영역으로 구분할 수 있다.

    a. 클래스 영역

        -> 실행에 필요한 클래스들을 로드하여 저장한다. 내부에서 메소드 영역과 상수 영역으로 또 나뉘어 저장 된다.

    b. 가비지 컬렉션 힙 영역

        -> GC에 의해 관리되는 영역이다. 동적 메모리 할당 영역 이라고도 하며, 소스상에서 new 연산자로 객체를 만들 때 할당되는 영역이다.

    c. 런타임 스택 영역

        -> 프로그램 실행 중 발생하는 메소드 호출과 복귀에 대한 정보를 저장한다.

    d. 네이티브 메소드 스택 영역

        -> 자바에는 하드웨어를 직접 제어하는 기능이 없기 때문에, 필요할 경우 C언어와 같은 다른 언어의 기능을 빌려 사용한다.

            이때 사용하는 기술이 JNI (Java Native Interface) 기술로 네이티브 메소드들이 바이트 코드로 변환 되면서 사용되고 기록하는 영역이다.

 

 

 

JDK 와 JRE 의 차이


JDK :: Java Development Kit -> 자바 개발 도구

JRE :: Java Runtime Environment -> 자바 실행 환경

 

즉, 자바 언어로 프로그램을 개발하기 위해서는 JDK를 설치해야 하고

자바 언어로 작성된 프로그램을 실행하기 위해서는 JRE를 설치해야 한다.

JDK를 설치하면 JRE가 포함되어 같이 설치된다. 이것과 관련해서 최근에 뭔가 달라진점이 있던 기억인데..

검색해도 기억이 나지 않는다..

 

 

 

 

728x90

'Archive > Java online live study S01' 카테고리의 다른 글

5주차 : 클래스  (0) 2021.05.01
4주차 : 제어문  (0) 2021.05.01
3주차 : 연산자  (0) 2021.05.01
2주차 : 자바 데이터 타입, 변수 그리고 배열  (0) 2021.05.01
Whiteship Java live study S01  (0) 2021.04.23
728x90

 

4. 자바 실행 환경 : 조금 더 즐거운 통합 개발 환경.


지금까지 알아본 내용을 토대로 프로그램 개발 과정을 생각해보자.

 

1. 메모장을 실행시킨다.

2. 프로그래밍 언어 (자바) 문법에 맞춰 원하는 프로그램을 작성한다.

3. 고급언어로 작성한 프로그램을 컴퓨터가 이해할 수 있도록 번역 (컴파일) 한다.

4. 오류가 발생했다면 오류를 수정한다.

5. 컴파일 결과물을 실행하기 위해 명령 프롬프트를 실행한다.

6. 명령 프롬프트에서 실행할 파일 (자바에서는 .class 확장자를 갖는 바이트 코드 파일)을 실행시킨다.

7. 결과를 확인한다.

 

생략된 과정들 (예를들면 컴파일 하기 위해 자바를 설치하고, 명령 프롬프트에서 컴파일 및 실행 시키기 위해 환경변수를 등록하는 일)도 있지만 대충 생각해도 꽤 많은 작업을 해야 비로소 프로그램을 실행시킬 수 있다.

 

 

 

생각해보면 2번 과정에서 프로그램을 어떻게 작성하는지에 대한 내용만 달라질 뿐, 그 외의 과정들은 프로그램을 만들 때마다 반복하고 있음을 알 수 있다.

프로그래밍 언어를 공부할 때 이 사실을 하나 기억하고 있으면 좋겠는 사실이 하나 있다.

일반적으로 했던 일을 또 하는 반복작업을 싫어하고 또 이러한 부분에 대해 내가 아닌 누군가 자동으로 해주길 바라고 있다는 것이다.

그리고 보통 이 피할 수 없는 반복 작업들을 피하는 방법의 패러다임은 (사견이지만) '사람이 할 일을 컴퓨터에게 시켜라' 이다.

 

 

첨언이 길었다.

본격적으로 위에서 말한 반복 작업을 어떻게 피하는지 통합 개발 환경을 구축하면서 생각해보자.

 

 

다양한 통합 개발 환경을 제공하는 프로그램들이 있지만 보편적으로 많이 사용하고 접하기 쉬운 eclipse IDE 로 알아보려 한다.

 

 

프로그램을 다운받기 위해 사이트로 이동한다.

 

 

그러면 현재 기준 위와 같은 화면을 볼 수 있다.

여기서 바로 다운로드 버튼을 누르지 말고 스크롤을 아래로 내려보자.

 

 

그러면 아래쪽에 Other에 IDE and Tools 링크가 존재한다.

이 링크를 눌러 이동하자.

 

 

위와 같은 화면이 나왔으면 Download 버튼을 클릭하자.

 

 

그러면 위와 같은 화면이 나타난다.

이클립스가 종류가 참 많다.

엔터프라이즈급 자바 개발을 위한 IDE, 자바 개발을 위한 IDE, C/C++ 개발을 위한 IDE 등등

자신에 맞는 이클립스를 다운 받으면 된다.

 

지금 화면은 가장 최신 버전을 먼저 보여주고 있는데, 이클립스마다 설치 되어 있어야 하는 JDK 버전이 다르므로 반드시 확인해야한다.

먼저 자신이 사용할 JDK 버전을 골랐으면 그 버전에 맞는 이클립스 버전을 MORE DOWNLOADS 에서 선택해서 다운받으면 된다.

 

 

추가적으로 이 화면의 위쪽을 보면 다음과 같은 Release 라는 링크가 있다.

 

 

링크를 클릭해보면 위와 같이 지금까지 배포한 버전들에 대한 링크가 존재한다.

각 버전들이 언제 출시 되었는지 다음 표를 참고하자.

위키백과에서 검색한 결과중 일부를 가져온 것이다.

 

 

안드로이드 버전에서도 볼 수 있는데, 버전명의 첫 문자가 알파벳 순서로 증가하는걸 볼 수 있다.

그냥.. TMI다.

 

 

 

아무튼 그래서 Mars 링크를 클릭하면 위와 같이 더 다양한 종류의 패키지들을 볼 수 있다.

처음 봤을때는 그냥 무조건 최신 버전을 받았던 기억이다.

정리하면 다음과 같다.

 

M Milestone
RC Release Candidates
R Release
SR Stable Builds
RTM Release to Manufacturing

 

RC같은 경우 정식 배포 하기 위한 후보군들에 붙인다.

위로 올라갈 수록 최신 버전으로 이전 버전들 보다는 안정적인 배포 버전이라 생각하면 될 것 같다.

개인적으로 버전의 네이밍 룰을 알고난 이후부터 되도록이면 R 버전을 다운받는다.


** eclipse build type 참고

 

 

얘기가 조금 다른 길로 샜는데 다시 돌아와서 Java EE Developer 의 윈도우 64비트 버전을 다운 받았다.

 

 

그러면 위와 같이 브라우저에 설정되어있는 경로에 이클립스가 다운 받아진다.

 

 

다운받은 파일의 압축을 풀면 위와 같은 파일들이 포함되어 있는걸 볼 수 있다.

누가 봐도 실행파일로 보이는 eclipse.exe 파일을 실행 시켜보자.

 

 

728x90

 

 

 

 

처음 실행 시키면 workspace 경로를 설정하라는 안내가 나온다.

실수로 생각없이 다시 묻지 않기를 선택해서 바로 welcome 페이지가 뜨면서 실행된 화면이다.

혹시 workspace 위치를 변경 하려면 다음과 같이 하면 된다.

 

 

 

workspace는 단어의 뜻 그대로 작업공간을 의미한다.

굳이 폴더 이름이 workspace일 필요는 없지만 편의상 이름을 수정하지는 않는다.

여기서 지정한 곳을 기준으로 작업한 내용이 저장된다.

 

 

앞서 메모장에서와 동일한 작업을 하는 프로그램을 만들기 위해 프로젝트를 생성해보자.

Project Explorer 영역에서 마우스 우클릭을 하고 New 의 Project를 선택한다.

만약 Project Explorer가 보이지 않는다면 이클립스 상단의 Quick Access를 활용하면 된다.

 

 

 

이렇게 바로 검색을 통해 찾아내는 방법이 있다.

그런데 만약 Quick Access를 찾을 수 없다면 다른 방법이 있다.

메뉴에서 Package Explorer를 찾아내 보여주는 방법이다.

 

 

상단의 Window > Show View > Package Explorer 를 바로 선택 하거나.

필요한 view가 목록에 없을 경우 맨 아래 Other... 를 선택한다.

 

 

Other... 를 선택하면 보여줄 수 있는 모든 View 목록이 나오는데, 위치를 안다면 찾아가도 된다.

만약 이름만 기억이 난다면 다음과 같이 검색을 통해 한번에 찾아내는 방법도 있다.

 

 

 

얘기가 조금 돌아갔지만, Package Explorer 영역에서 New > Project 를 선택하면 위와 같은 창이 뜬다.

 

 

어디에 있는지 알면 바로 선택해도 되고, 그게 아니면 검색을 통해 Java Project를 찾아 선택한다.

 

 

Java Project를 선택하면 프로젝트 생성 마법사 창이 뜬다.

보이는 항목 중에 Use default location 을 보면 앞서 설정한 workspace 경로가 입력 되어 있다.

체크박스를 해제하면 다른 위치에 만들 수 있겠지만 추천하지는 않는다.

 

 

Project name을 기입하면 아래 생성되는 위치가 실시간으로 바뀌고 있는것을 볼 수 있다.

그리고 JRE 버전도 프로젝트 생성시 설정할 수 있다.

Project layout 항목을 보면 생성한 프로젝트 최상위 위치에 소스파일과 클래스 파일을 같이 둘지

아니면 소스와 클래스 파일을 서로 다른 폴더에 구분할지 설정하는 부분이다.

 

설정이 완료 되었으면 Finish 버튼을 선택한다.

 

 

그러면 알림창이 하나 뜨면서 perspective 를 변환할 것인지 물어본다.

perspective는 사전적으로 어떠한 관점을 뜻하는 단어다.

그런 의미에서 본다면 앞으로 할 작업을 어떤 관점에서 하겠냐고 이해하면 좋겠다.

 

 

eclipse IDE의 perspective는 보통 우측 상단에 표시된다.

현재 Java EE 로 설정 되어 있으므로 단순 Java 프로젝트를 위한 Java perspective로 전환할 것인가를 물어보는 것이다.

 

 

그렇게 하겠다고 Yes 버튼을 클릭하면 우측 상단의 Java perspective가 생기며 활성화 되는 것을 볼 수 있다.

그러면서 크게 차이는 못느낄수 있지만 Java 개발에 최적화된 기본 View 들을 제공해주고 있다.

얼마든지 언제든지 작업 환경에 따라 바꿔줄 수 있으므로 부담 가지지 말자.

 

 

이제 좌측에 보면 TestPrj 이름을 갖는 Java 프로젝트가 생성된 것을 볼 수 있다.

 

 

그럼 이번엔 앞서 메모장에서 만들었던 것과 동일한 작업을 하는 소스 파일을 생성해보자.

src를 마우스 우클릭을 하고 New > Class 를 선택한다.

 

 

파일이 생성될 Source folder 경로가 나오며 해당 위치에 생성할 소스 파일의 이름을 지정해주도록 되어 있다.

또한 중간 쯤에 보면 Which method stubs would you like to create? 항목이 있다.

이것들은 IDE에서 제공하는 기능으로 자바 파일 생성시 빈번하게 반복하는 작업을 대신 해줄까? 하는 내용이다.

선택을 해도 좋고 안해도 상관 없다.

 

 

Finish를 클릭하면 TestPrj 프로젝트 하위에 TestClass.java 파일이 생성되는 것을 볼 수 있다.

 

 

eclipse 실행시 설정했던 workspace 경로를 따라가서 생성한 프로젝트 하위의 src 폴더를 열어보면 파일이 생성되어 있는것을 볼 수 있다.

 

 

앞서 메모장에서 테스트한 내용을 기억할지 모르겠다.

그때는 .java 파일을 javac 컴파일 명령어로 처리를 했어야 .class 파일이 만들어졌다.

하지만 이클립스 IDE를 사용하면 (설정에서 안하게 할 수도 있지만, 기본 설정을 수정하지 않으면) 위와같이 bin이라는 경로에 .class 파일이 자동으로 생긴 것을 확인할 수 있다.

 

 

앞에서 다루었던 Hello, World! 문자열을 출력한 예제를 작성해보자.

 

 

이것 역시 메모장에서는

1. 명령 프롬프트 실행 (cmd)

2. .java 파일의 경로로 이동

3. javac 명령으로 컴파일

4. .class 파일을 java 명령으로 실행

하는 과정을 거쳐야 실행 결과를 받아볼 수 있었다.

 

하지만 이클립스 IDE에서는 실행과 관련된 Run 메뉴 하위의 Run As > Java Application 을 선택하면 된다.

 

 

그러면 이클립스 자체에 Console 이라는 view 가 생기면서 그곳에 실행 결과가 출력된다.

정말 편리하지 않은가?

심지어 ctrl + F11 키를 누르거나 Alt + Shift + X, J 단축키로도 한번에 실행할 수 있다.

 

메모장으로 프로그램을 작성할 때보다 훨씬 시간과 노력이 줄어드는것을 대출 봐도 알 수 있다.

그럼에도 처음에는 메모장에서 프로그램을 작성하는 연습을 하기를 추천하다.

 

 

 

이번에는 마지막으로 괜히 명령프롬프트를 활용해서 .class 파일을 찾아가 실행시킨 화면이다.

어떻게 실행시켜도 상관 없다는것을 보여주고 싶었다.

 

 

지금까지 나름의 자바 정리를 해봤다.

처음부터 되도록이면 다 설명을 해보고 싶은 욕심에 전달이 잘 됐을지 걱정이다.
한창 정리하다가 그만하게 될까도 걱정이었는데 그래도 어떻게든 마무리 하게 되어 다행이다.

 

 

 

 

728x90

+ Recent posts