자바(2), 객체 지향 프로그래밍

2022. 8. 29. 23:27자바

728x90
반응형

이번 포스팅 역시 자바와 C++과의 객체 지향 프로그래밍 중 차이점을 위주로 내용을 작성해보고자 한다. 자바는 C++과 유사한 면이 많기에 차이점을 위주로 학습하면 더욱 기억에 잘 남을 것이라 생각한다.


1. 객체 사용

1) 객체 지향 프로그래밍

객체 지향 프로그래밍의 핵심 원칙 중 하나는 캡슐화이다. 이는 모든 객체지향 프로그래밍이 동일하게 적용되는 원칙으로, 다른 사람이 구현한 객체의 메서드를 호출할 때는 내부에서 무슨 일이 일어나는지 몰라도 된다는 원칙이다. 

 

자바에서는 클래스를 사용해 객체를 생성하고 사용할 수 있다. 

 

2) 접근자 메서드와 변경자 메서드

메서드에서 데이터를 수정하는 방법은 두 가지 방법이 있다. 첫번째 방법은 멤버 함수로 전달받은 객체의 상태를 변경하고 아무 것도 반환하지 않는 방식이다. 파이썬을 예시로 든다면 sort함수가 되겠다. 이는 변경자라고 한다. 두번째 방법은 멤버 함수로 전달받은 객체를 변경하지 않고 새로 생성된 객체를 반환하는 방식이다. 이 또한 파이썬을 예시로 든다면 sorted함수가 있다. 이는 접근자라고 한다.

변경자: 호출되는 객체를 변경하는 매서드

접근자: 객체를 변경하지 않는 메서드

 

객체 변경의 경우 두 연산이 객체 하나를 동시에 변경할 때 위험할 수 있으므로 접근자로 사용하는 형태가 보편화되고 있다.

 

3) 객체 참조

C++은 변수에 실제 객체를 담을 수 있다. 반면 자바에서는 오직 객체 참조만 담을 수 있다. 다시 말해 실제 객체는 다른 곳에 있고, 참조는 실제 객체를 찾아내는 구현체 고유의 방법을 사용한다. 이는 C++의 포인터와 유사하게 작동하지만 C++의 경우 임의의 메모리 위치를 덮어쓸 수 있다는 위험성이 있지만 자바의 참조로는 특정 객체에만 접근할 수 있다. 

 

값을 할당한 뒤, 다른 값으로 재할당한 경우에는 자바의 가비지 컬렉터가 메모리를 정리해서 재사용할 수 있게 한다. 자바는 이 과정이 자동으로 일어나므로 프로그래머는 메모리 할당 해제를 걱정할 필요가 없다.

 


2. 클래스 구현

1) 인스턴스 변수

public class Employee{
	private String name;
	private double salary;
    ...
}

인스턴스 변수로 객체의 상태를 나타낼 수 있다. 일반적으로 자바에서는 인스턴스 변수를 private으로 선언한다. 이렇게 선언한다면 같은 클래스에 속한 메서드만 변수에 접근할 수 있기에 어느 부분이 변수를 변경할 수 있는지 제어할 수 있다.

 

 

2) 메서드 헤더와 바디

public void raise Salary(double byPercent){
	double raise = salary * byPercent / 100;
    salary += raise;
}

public String getName() {
	return name;
}

메서드를 선언할 때는 메서드 이름, 애개변수의 타입과 이름, 반환 타입을 지정해야한다. 대부분의 메서드는 public으로 선언한다. 하지만 때론 헬퍼 메서드는 private으로 선언해 같은 클래스에 속한 다른 메서드에서만 사용가능하도록 제한한다.

 

메서드에서 값을 돌려줄 때는 return 키워드를 사용한다. 또한 메서드의 선언은 클래스 선언 안에 넣어야한다.

 

public class Employee {
	private String name;
    private double salary;
    
    public void raiseSalary(double byPercent) {
    	double raise = salary * byPercent / 100;
        salary += raise;
    }
    
    public String getName() {
    	return name;
    }
    ...
}

 

3) 인스턴스 메서드 호출과 this 참조

자바에서 static으로 선언하지 않은 메서드는 모두 인스턴스 메서드다. 객체의 메서드를 호출할 때 해당 객체가 this로 설정된다. 원한다면 메서드를 수현할 때 this 참조를 사용해도 된다. 대게는 매개변수의 이름과 인스턴스 변수의 이름이 동일할 때 this 참조를 사용한다.

public class Employee {
	private String name;
    private double salary;
    
    public void raiseSalary(double byPercent) {
    	double raise = this.salary * byPercent / 100;
        this.salary += raise;
    }
    
    public String getName() {
    	return name;
    }
    ...
}

 

 

4) 값을 사용한 호출

메서드에 객체를 전달하면 해당 메서드는 객체 참조의 사본을 얻는다. 메서드는 이 참조로 매개변수 객체에 접근하거나 매개변수 객체를 변경할 수 있다. 따라서 자바는 기본 타입 값은 물론 객체 참조까지 모든 매개변수가 값으로 전달된다.

 


3. 객체 생성

1) 생성자 구현

생성자는 메서드와 비슷하지만 생성자의 이름은 클래스와 같고, 반환 타입은 없다. 실수로 반환타입을 명시한다면 이는 메서드를 선언하는 것이 된다.

public class Employee {
	private String name;
    private double salary;
    
    // 생성자 구현
    public Employee(String name, double salary) {
    	this.name = name;
        this.salary = salary;
    }
    
    public void raiseSalary(double byPercent) {
    	double raise = salary * byPercent / 100;
        salary += raise;
    }
    
    public String getName() {
    	return name;
    }
    ...
}

 

생성자는 new 연산자를 사용한 시점에 실행된다. 이때 클래스의 객체를 할당한 후 생성자 바디를 호출한다. 그리고 생성자 바디는 생성자에 전달된 인수로 인스턴스 변수를 초기화한다.

 

 

2) 오버로딩

오버로딩을 통해 호출되는 생성자는 인수에 따라 결정해줄 수 있다.

public class Employee {
	private String name;
    private double salary;
    
    public Employee(String name, double salary) {
    	this.name = name;
        this.salary = salary;
    }
    
    // 생성자 오버로딩
    public Employee(String name) {
    	this.name = name;
        this.salary = 0;
    }
    
    public Employee(double salary) {
    	this("", salary); // Employee(String, double)을 호출한다.
        // 이후에 다른 문장이 올 수 있다.
        // 여기서 this는 생성될 객체 참조가 아닌 생성자를 호출하는 문법이다.
    }
    
    
    public void raiseSalary(double byPercent) {
    	double raise = salary * byPercent / 100;
        salary += raise;
    }
    
    public String getName() {
    	return name;
    }
    ...
}

다른 생성자에서 특정 생성자를 호출하기 위해선 this 키워드를 사용해 이를 호출할 수 있다. 이때 this는 같은 클래스에 속한 다른 생성자를 호출할 때 사용하는 특수 문법이다.

 

3) 기본 초기화

인스턴스 변수를 생성자 내에서 명시적으로 설정하지 않으면 변수는 기본 값으로 설정된다. 숫자는 0, 불 값은 false, 객체 참조는 null이 기본 값이다. 만약 null인 값에 대해 메서드를 호출해 사용한다면 널 포인터 예외가 발생한다.

 

4) 인스턴스 변수 초기화

public class Employee {
	private final String name; // final 변수 선언
    private double salary = 0; // 초깃값
    private int id;
    
    { // 초기화 블록
    	Random generator = new Random();
	    id = 1 + generator.nextInt(1000000);
    }
    
    public Employee(String name, double salary) {
    	this.name = name;
        this.salary = salary;
    }
    
    // 생성자 오버로딩
    public Employee(String name) {
    	this.name = name;
        this.salary = 0;
    }
    
    public Employee(double salary) {
    	this("", salary);
    }
    
    
    public void raiseSalary(double byPercent) {
    	double raise = salary * byPercent / 100;
        salary += raise;
    }
    
    public String getName() {
    	return name;
    }
    ...
}

클래스의 선언 안에 초깃값을 지정해주거나 임의의 초기화 블록을 넣는 방법이 있다. 

최종 인스턴스 변수 또한 선언할 수 있다. 하지만 최종으로 선언한 변수는 생성자 실행이 끝나기 전에 초기화해야한다. 초기화를 한 후에는 해당 변수를 수정할 수 없다.

 

5) 인수 없는 생성자

인수 없는 생성자는 기본 생성자로, 클래스 내에 생성자가 없다면 이를 자동으로 받지만 생성자를 작성했다면 인수 없는 생성자를 만들어야한다.

 


4. 정적 변수와 정적 메서드

1) 정적 변수 (클래스 변수)

static 변수로 선언하면 해당 변수는 클래스당 하나만 존재한다. 반면 각 객체에는 자체적인 인스턴스 변수의 사본이 들어있다.

public class Employee {
	private static int lastId = 0; // Id 정적 변수로 설정
    private int id; 
    ...
    public Employee() {
    	lastId++; // 생성자 호출 시 id값 갱신
        id = lastId;
    }
}

위 예제에서 lastId 변수는 클래스의 특정 인스턴스가 아닌 클래스 자체에 속한다. 이때 모든 직원 객체가 유일한 id 값을 얻는다. 단, 여러 스레드가 Employee 객체를 동시에 생성하면 이 코드는 제대로 동작하지 않는다. 병렬 프로그래밍이 이 문제를 해결할 수 있다.

 

2) 정적 상수

변경 가능한 정적 변수는 드물지만 정적 상수(static final)은 일반적이다. 이때 모든 클래스의 객체는 자체적으로 정적 상수의 사본을 가지고 있어야한다. 

 

3) 정적 초기화 블록

정적 변수를 선언하면서 초기화하는 것을 넘어 초기화 작업이 추가로 필요할 때도 있다. 이러한 경우에는 정적 초기화 블록을 통해 정적 변수를 선언할 수 있다. 

public class CreditCardForm {
	private static final ArrayList<Integer> expirationYear = new ArrayList<>();
    
    // 정적 초기화 블록
    static {
    	// 다음 20개 연도를 배열 리스트에 추가한다.
        int year = LocalDate.now().getYear();
        for (int i = year; i <= year + 20; i++) expirationYear.add(i);
    }

}

정적 초기화는 클래스를 처음 로드할 때 일어난다.

 

4) 정적 메서드

클래스의 객체를 만들지 않고 클래스 내의 메서드를 호출하기 위해 정적 메서드를 사용한다. 또한 다른 사람이 만든 클래스에 부가 기능을 제공하는 경우에도 정적 메서드를 사용한다. 아래에 두번째 사용 이유에 대한 예시 코드가 있다.

public class RandomNumbers {
	public static int nextInt(Random generator generator, int low, int high) {
    	return low + generator.nextInt(high - low + 1);
    }
}

// 호출
int dieToss = RandomNumbers.nextInt(gen, 1, 6);

정적 메서드는 객체에 작동하지 않기에 인스턴스 변수에 접근할 수 없다. 하지만 정적 변수에는 접근할 수 있다.

 

5) 팩토리 매서드

클래스의 새 인스턴스를 반환하는 정적 메서드를 의미한다. 매개변수가 없는 생성자는 2개 이상 만들 수 없기에 생성자 대신 팩토리 매서드를 사용한다. 팩토리 매서드는 새 인스턴스를 반환할 때 새 객체를 생성하는 대신 공유 객체를 반환할 수도 있다.


5. 패키지

1)  패키지 선언

자바에서 패키지는 중첩되지 않는다. java.util과 java.util.regex 패키지는 유사해보이지만 서로 관련이 없는 독립적인 클래스의 묶음이 각각에 들어가 있다.

 

패키지 이름이 유일함을 보장하려면 유일하다고 알려진 인터넷 도메인 이름을 뒤집어서 사용하는 것이 좋다. 예를 들어 konghana.com이라는 도메인 이름을 가지고 있다면 해당 프로젝트를 진행할 때 com.konghana.corejave와 같은 패키지 이름을 사용하는 것이 좋다. 

 

간단한 프로그램에 사용하는 이름 없는 기본 패키지 개념도 있다. 하지만 파일 시스템에서 클래스 파일을 읽어 올 때 경로의 이름은 패키지의 이름과 일치해야하기에 이를 사용하지 않는 것을 권장한다.

 

2) 패키지 접근

public이나 private을 지정하지 않으면 같은 패키지에 속한 모든 메서드에서 접근할 수 있다. 이러한 패키지의 개방성 때문에 문제가 발생할 여지가 있을 수 있다. 이는 패키지를 모듈에 넣어 해결할 수 있다. 패키지가 모듈 안에 있으면 클래스를 해당 패키지에 추가할 수 없다. 

 

소스 파일 하나에 여러 클래스를 포함할 수 있지만 최대 한개만 public으로 선언할 수 있다. 소스 파일에 공개 클래스가 있을 때는 파일 이름이 공개 클래스 이름과 일치해야한다.

 

3) 클래스 임포트

import 문을 사용하면 전체 이름을 쓰지 않아도 클래스를 사용할 수 있다. 이는 C++의 using 기능과 유사하다고 볼 수 있다. 와일드카드(*)를 사용하면 패키지에 들어있는 모든 클래스를 임포트할 수 있다. 만약 패키지들 간의 이름 충돌이 있는 경우에는 특정 클래스를 지정해 임포트하면 된다.

// 패키지 간 이름 충돌 발생
import java.util.*;
import java.sql.*;

// 이름 충돌 해결
import java.util.*;
import sql.*;
import java.sql.Date; // 중복되는 클래스의 이름을 지정해 임포트

 

4) 정적 임포트

import static java.lang.Math.*;

 


6. 중첩 클래스

1) 정적 중첩 클래스

클래스 내부에 클래스를 만들어 사용하는 것이다. 이는 외부에서 선언한 클래스와의 인터페이스를 고려해 조합을 선택하는 경우와 크게 다르지 않다. 

public class invoice {
	private static class Item { // Invoice 내부에 Item을 중첩했다.
    	String description;
        int quantity;
        double unitPrice;
        
        double price() {return quantity * unitPrice}
    }
    ...
}

 

 

2) 내부 클래스

클래스 내부에 클래스가 선언되어있지만 static을 붙이지 않은 클래스이다. static 제어자를 빼는 경우에는 각 객체에 대한 정보를 파악할 수 있다는 점을 파악할 수 있다.

 

내부 클래스에는 컴파일 시간 상수 외에 정적 멤버를 선언할 수 없다.

 

3) 내부 클래스용 특수 문법 규칙

외부 클래스의 숨은 참조를 'OuterClass.this'라는 표현으로 외부 클래스 참조를 나타낼 수 있다. 혹은 외부 클래스의 객체만 참조해도 암묵적으로 외부 클래스를 참조한다. 하지만 외부 클래스 참조가 명시적으로 필요한 순간에는 위와 같이 참조해야한다.

728x90
반응형