자바 문법 공부 끄적끄적

동준·2023년 11월 27일
0

개인공부(자바)

목록 보기
1/16

1. 타입

1) String : 참조

String str1 = "문자열";
String str2 = "문자열";
String str3 = new String("문자열");

str1 == str2; // true
str1.equals(str2); // true
str1 == str3; // false
str1.eqauls(str3); // true

문자열 역시 객체이므로 참조 주소를 가지고 있음. == 은 참조 주소를 비교하고 equals() 메소드는 참조 주소(즉, 동일한 객체이든 다른 객체이든) 상관없이 내부 값만 비교하고자 할 때 사용

그러므로 문자열의 내부 값 비교할 때는 equals() 메소드 쓰자

String subject = "안녕 반갑습니다";

char charValue = subject.charAt(3); // 반
int length = subject.length(); // 8(공백 포함)

String newStr = subject.replace("안녕", "헬로우"); // 수정본이 아닌 새로운 문자열
System.out.println(subject); // "안녕 반갑습니다"
System.out.println(newStr); // "헬로우 반갑습니다"

// subString(int beginIndex, int endIndex)
// endIndex가 없으면 자동으로 시작 인덱스에서 맨 마지막 인덱스까지)

int index = subject.indexOf("반갑습니다") // 3("반갑습니다" 문자열이 인덱스 3부터 시작)

String은 참조이므로 초기값을 정하지 않으면 null로 초기화된다


2. 객체 지향 프로그래밍

객체 = 속성동작으로 분류

  • 속성 : 특정할 수 있는 개체(이름, 나이 등등...) -> 필드
  • 동작 : 영어 구문의 동사 같은(달리다, 먹다 등등...) -> 메소드

객체와 객체 간의 상호작용은 메소드가 담당

  • 메소드는 매개변수를 요구할 수도 있다.
  • 메소드의 리턴값은 변수에 대입 저장 가능.

객체 간의 관계

  • 사용 관계 : 자동차사람
  • 집합 관계 : 자동차엔진, 타이어, 핸들
  • 상속 관계 : 자동차기계

객체 지향 프로그래밍의 특징

  • 캡슐화 : 접근 제어자
  • 상속 : 코드 재사용성 및 유지 보수시간 향상
  • 다형성 : 타이어의 규격 -> 한국타이어, 금호타이어... (인터페이스)

스택과 힙

  • 스택(Stack) 영역:
    객체의 메서드를 호출할 때, 해당 객체에 대한 정보와 함께 호출 스택에 새로운 프레임이 쌓여. 이 프레임에는 메서드가 실행되는 동안 필요한 지역 변수, 매개 변수, 반환 주소 등이 저장돼. 메서드가 끝나면 해당 프레임이 스택에서 제거돼. 여기서 스택은 마치 함수 호출을 추적하는 일종의 기록장부 같은 역할을 한다고 생각해.
  • 힙(Heap) 영역:
    객체는 힙에 할당돼. 객체가 생성될 때마다 힙에 새로운 공간이 할당되고, 해당 객체의 데이터와 속성이 여기에 저장돼. 객체가 더 이상 필요하지 않으면 개발자가 명시적으로 메모리에서 해제해야 해. 자바스크립트에서는 가비지 컬렉터가 알아서 더 이상 참조되지 않는 객체를 확인하고 메모리에서 제거해줘.
    간단히 말하면, 객체의 메서드 호출과 관련된 정보는 스택에 임시로 저장되고, 실제 객체의 데이터와 속성은 힙에 할당되어 메모리를 차지한다. 이 둘은 함께 작동하여 프로그램이 실행될 때 데이터를 효과적으로 관리하고 사용할 수 있게 해준다.

3. 클래스

  • 생성자 작성을 하지 않아도 바이트코드 파일에서는 기본 생성자가 알아서 생성
  • this의 뜻은 해당 인스턴스의~
// 소스 파일(Car.java)
public class Car {

}

// 바이트코드 파일
public class Car{

	public Car() {
   
	}
}

1) 메소드와 필드

  • 메소드 오버로딩 : 매개변수 달리해서 객체 필드 다양하게 초기화 가능(심지어 생성자도)
    리턴 타입은 무관하고, 메소드명은 동일하며, 매개변수의 타입, 개수, 순서가 다르게 되어야 오버로딩이라고 할 수 있음
  • 가변 길이의 매개변수 : 매개변수 개수 상관없이 매개값 부여 가능
pulbic int sum (int ... values) {

	int sumValue = 0;
	
	for(int num : values) {
	  	sumValue = sumValue + num;
    }
  
  	return sumValue;
}

인스턴스 생성 시, 필드는 해당 객체에 소속이 되지만 메소드는 메소드 영역에 별도로 두고 객체 없이는 사용하지 못하게 둔 셈(만약 인스턴스마다 메소드가 저장되면 메모리 효율이 급속 저하될 터)

return 문은 메소드를 종료하는 역할을 하며, 반환 타입이 void 인 메소드에서도 사용할 수 있습니다. void 는 반환하는 값이 없음을 나타내지만, 메소드 실행 중간에 return 을 통해 메소드를 종료시킬 수 있습니다. 이 경우, 실제로는 값을 반환하지 않고 메소드 실행을 중지합니다.

생성자

다른 생성자 호출? -> 생성자 오버로딩이 많아지면 중복 코드 발생할 터
이때 쓸 수 있는 개념이 this()를 통한 생성자 체이닝

코드 중복이 발생할 수 있는 생성자 오버로딩 중복 사례

Car(String model) {
	this.model = model;
    this.color = "은색";
    this.maxSpeed = 250;
}

Car(String model, String color) {
	this.model = model;
    this.color = color;
    this.maxSpeed = 250;
}

Car(String model, String color, int maxSpeed) {
	this.model = model;
    this.color = color;
    this.maxSpeed = maxSpeed;
}

// 무수한 생성자 오버로딩....

이것을 해결할 수 있는 this()를 통한 생성자 체이닝

Car(String model) {
	this(model, "은색", 250); // 맨 밑의 생성자 호출
}

Car(String model, String color) {
	this(model, color, 250); // 맨 밑의 생성자 호출
}

Car(String model, String color, int maxSpeed) {
	this.model = model;
    this.color = color;
    this.maxSpeed = maxSpeed;
} // 공통 초기화 코드

// 생성자 체이닝

정적 메소드

  • 객체마다 가지고 있을 필요 없는 공용적인 필드는 정적 필드로 선언하기
  • 생성자는 객체 생성 후에 실행되므로 정적 필드는 생성자에서 초기화 작업 x

정적 블록 사용례

public class Television {
	static String company = "애플";
	static String model = "LCD";
    static String info;

	static {
    	info = company + "-" + model;
    }
}

public class TelevisionExample {
	public static void main(String[] args) {
    	System.out.println(Television.info); // 애플-LCD
    }
}

info 처럼 복잡한 초기화 작업이 필요할 때, 정적 블록을 사용한다.\

정적 메소드와 정적 블록 내부에 인스턴스 메소드나 필드 사용 불가

public class Example {
    // 인스턴스 필드와 메소드 선언
   int field;
   void method1(){
       
   };
   
    // 정적 필드와 메소드 선언
    static int field2;
    static void method2() {
       
    };
   
    // 정적 블록 선언
    static {
        field1 = 10; // (x)
        method1(); // (x)
        field2 = 10;
        method2();
    }
   
    // 정적 메소드 선언
    static void method3() {
        this.field1 = 10; // (x)
        this.method1(); // (x)
        field2 = 10;
        method2();
    }
}

정적 메소드와 정적 블록에서 인스턴스 멤버 사용하려면 객체 먼저 생성하고 참조 변수로 접근하기(항상 컨트롤러 메인 메소드로 임포트해서 쓸 때 예시로 생각하기)

final 만 쓴다고 상수가 아니다.
상수는 객체마다 저장할 필요가 없고, 여러 개의 값을 가져도 안 되므로 static 이면서 final 인 특성을 가져야 함

접근 제한자

  • public : 제한 없음
  • protected : 같은 패키지 내에서, 상속 관계에서
  • default : 같은 패키지 내에서만
    // default는 접근 제한자가 아니라 그냥 접근 제한자가 붙지 않은 상태를 말함
  • private : 오로지 해당 객체 내부

생성자도 접근 제한자 가짐(정적 클래스에서 인스턴스화 방지 위해서 private로 생성자 선언했던 거 생각)

2) 싱글톤 패턴

앱 전체에서 단 한 개의 객체만 생성해서 사용하고 싶다면 싱글톤 패턴을 적용

핵심 포인트

private 클래스() {} 
// 생성자를 private로 지정해서 외부에서 new 연산자로 호출 x

싱글톤 패턴의 전체 코드 개요

public class ExampleClass {
	// private 접근 권한을 갖는 정적 필드 선언과 초기화
	private static ExampleClass singleton = new 클래스();
  
    // private 접근 권한을 갖는 생성자 선언
    private ExampleClass() {}

	// public 접근 권한을 갖는 정적 메소드 선언
	public static ExampleClass getInstance() {
    	return singleton;
	}
}

이를 통해, 외부에서 ExampleClass 객체를 얻는 방법은 getInstance 메소드를 호출하는 것 뿐. 이 때, getInstance 메소드의 리턴값인 객체가 싱글톤 객체

  • 싱글톤 객체는 결국 하나의 클래스에 대해 단일 인스턴스를 생성하게 되므로 외부 클래스들에서 생성한 여러 싱글톤 객체의 참조주소는 전부 동일
  • 전역 상태 관리:
    싱글톤은 어플리케이션 전역에서 단일 상태를 관리하고 유지할 수 있습니다. 이를 통해 여러 부분에서 동일한 상태에 접근하고 수정할 수 있습니다.
  • 인스턴스 제한:
    특정 클래스의 인스턴스를 하나로 제한하고 싶을 때 사용됩니다. 예를 들어, 데이터베이스 연결, 설정 정보, 로깅 등의 경우에 싱글톤 패턴을 사용하여 단일 인스턴스를 유지합니다.
  • 레이지 로딩(Lazy Loading):
    인스턴스를 필요한 시점에만 생성하여 불필요한 초기화를 피하고 성능을 향상시킬 수 있습니다. 이는 객체가 처음으로 필요할 때만 생성되도록 하는 레이지 로딩(Lazy Loading) 방식으로 구현될 수 있습니다.
  • 싱글 포인트 제어:
    어플리케이션에서 특정 기능이나 서비스를 제공할 때, 싱글톤 패턴을 사용하여 해당 서비스에 대한 단일 포인트를 제공할 수 있습니다.

데이터베이스 접근 연결 관리, 로그 관리, 설정 관리 등에서 주로 쓰임

하지만 아직 필요성이 체감되지 않는... 음... 우테코 프리코스 리팩토링에서 쓸 수 있을까

3) 중첩 선언

멤버 클래스, 로컬 클래스

public class Nested {

    public Nested() {
        class Local2 {
            //TODO : 생성자 내부에도 로컬 클래스 선언 가능
        }
    }

    //TODO : 인스턴스 멤버 클래스
    class Member1 {
        // 인스턴스 멤버 클래스니까 Nested 객체 생성해야 member1 객체 생성 가능
        int field1 = 1;

        static int field2 = 2; // Java 17부터 가능

        Member1() {
            System.out.println("Member1 생성자 실행");
        }

        void method1() {
            System.out.println("내부 인스턴스 메소드 실행");
        }

        static void method2() {
            System.out.println("내부 정적 메소드 실행");
        }
    }

    //TODO : 정적 멤버 클래스
    static class Member2 {
        // 정적 멤버 클래싀까 Nested 객체 생성하지 않아도 member2 객체 생성 가능
    }

    void method() {
        //TODO: 로컬 클래스
        class Local {
            // 우선 정적 메소드 아니니까, Nested 객체부터 생성해야 됨
            // 그래야 method 메소드 사용 가능한데, 그제서야 local 객체 생성 가능
        }

        Member1 member1 = new Member1();
        System.out.println(member1.field1);
        member1.method1();

        System.out.println(Member1.field2);
        Member1.method2();
    }
}
  • 인스턴스 멤버 클래스는 일반적으로 클래스 내부에서 쓰이므로 private 접근 제한이 일반적임
  • 인스턴스 멤버 클래스는 바깥 클래스 내부 어디에서나 생성할 수 없고, 인스턴스 필드값, 생성자, 인스턴스 메소드에서 생성할 수 있음.
    • 바깥 클래스가 있어야 인스턴스 멤버 클래스도 생성할 수 있음
// 바깥 클래스 A, 인스턴스 멤버 클래스 B
A a = new A();
A.B b = a.new B();
  • 정적 멤버 클래스는 바깥 클래스 객체 생성 불필요
// 바깥 클래스 A, 정적 멤버 클래스 C
A.C c = new A.C();
  • 생성자나 메소드 내부에서 선언된 클래스를 로컬 클래스라고 함
  • 로컬 클래스에는 static 키워드를 이용해서 정적 클래스로 못 만듦
// 바깥 클래스 A, 로컬 클래스(메소드 useD() 내부) D
A a = new A();
a.useD();
  • 로컬 변수 : 생성자 또는 메소드 매개변수, 메소드 내부 선언 변수
    • 로컬 변수를 로컬 클래스에서 사용할 경우, 로컬 변수는 final 특성을 갖게 된다. (안 붙여도 final 키워드가 붙여짐)
    • 로컬 클래스 내부에서 값을 변경하지 못 하도록 제한

바깥 클래스의 멤버 접근 제한

  • 인스턴스 멤버 클래스는 바깥 클래스의 모든 필드와 메소드 사용 가능
  • 정적 멤버 클래스는 바깥 클래스의 정적 필드정적 메소드만 사용 가능

익명 객체

익명 객체는 직접 생성자 선언 불가능

4) Object 클래스

모든 클래스의 어머니(?)

Object 클래스의 메소드

boolean equals(Object obj) // 객체의 번지를 비교하고 같은 지를 리턴
int hashCode() // 객체의 해시코드를 리턴
String toString() // 객체의 문자 정보를 리턴
  • 해시 코드 == 객체의 나사빠진 주민등록번호(?)
    hashcode()는 기본적으로 객체의 해시코드 번호를 반환하는데, 모든 객체는 각자의 고유한 해시코드 번호를 지니며 설령, 변수명이나 내부 내용(메소드, 필드 등등)이 같다고 한들 다른 해시코드 번호를 구별 보유하는, 이른바 주민등록번호 같은 역할을 한다. 다만 초기화가 동일하게 이뤄진 동일 인스턴스 클래스 객체는 둘 다 해시 코드가 같다.
  • 그렇지만 두 개 이상의 동일한 해시 코드를 지닌 객체가 있을 수 있다.
    해시 함수는 객체가 가지는 데이터를 해시 코드로 변환하는 작업을 수행한다. 그렇기에 발생할 수 있는 값은 유한하며 해시 함수의 출력 범위가 입력보다 작기 때문에 서로 다른 객체여도 동일한 해시 코드를 지니는 해시 충돌이 일어날 수도 있다.
  • 두 객체의 일차 여부는 해시 코드가 아닌 동등성의 개념으로
    해시 코드가 같더라도 동등성이 꼭 참일 수는 없다

3. 다형성

1) 상속에서의 다형성

다형성 = 자동 타입 변환 + 메소드 오버라이딩

자동 타입 변환

class Animal {
	void method1() { }
    void method2() { }
}

class Cat extends Animal {
	@Override
    void methpd2() { }
    void method3() { }
}

Cat cat = new cat();
Animal animal = cat;

cat == animal // true(타입만 다를 뿐, 동일한 Cat 객체를 참조하고 있음)
Animal animal = new Cat(); // 요렇게도 가능(보편적으로 쓰기)
public class AnimalExample {
	public static void main(String[] args) {
  	Cat cat = new Cat();
	Animal animal = new Animal();

    animal.method1(); // true
    animal.method2(); 
    // true, 대신 자식 클래스(Cat)에서의 오버라이딩된 메소드 실행
    // 이걸 사용해서 다형성이 구현되는 것

    animal.method3(); // 불가능!

    }
}

강제 타입 변환

Animal animal = new Cat(); // 자동 타입 변환
Cat cat = (Cat) animal; // 강제 타입 변환

자식 객체가 부도 타입으로 자동 변환되면 부모 타입에 선언된 필드와 메소드만 사용 가능하다는 제약 사항이 붙기 때문에, 만약 자식 타입에 선언된 필드와 메소드를 다시 쓰고 싶으면 강제 타입 변환이 쓰인다.

다형성 적용 - 필드

public class Car {
	public Tire tire;
   
    public void run() {
    	tire.roll;
    }
   
public class Tire {
	public void roll() {
    	System.out.println("회전합니다.");
    }
}

public class HankookTire extends Tire {
	@Override
    public void roll() {
    	System.out.println("한국타이어 회전합니다.");
    }
}

public class KumhoTire extends Tire {
	@Override
    public void roll() {
    	System.out.println("금호타이어 회전합니다.");
    }
}

// 다형성 적용 - 필드
public class CarExample {
	public static void main(String[] args) {
    	Car myCar = new Car(); // Car 객체 생성
      
        myCar.tire = new Tire(); // Tire 객체 장착
        myCar.run(); // "회전합니다"

        myCar.tire = new HankookTire(); // 한국타이어 객체 장착
        myCar.run(); // "한국타이어 회전합니다."

        myCar.tire = new KumhoTire(); // 금호타이어 객체 장착
 		myCar.run(); // "금호타이어 회전합니다."
    }
}

다형성 적용 - 매개변수

public class Driver {
	public void drive(Vehicle vehicle) {
    	vehicle.run();
    }
}

public class Vehicle {
	public void run() {
    	System.out.println("차량이 달립니다.")
    }
}

public class Bus extends Vehicle {
	@Override
    public void run() {
    	System.out.println("버스가 달립니다.")
    }
}

public class Taxi extends Vehicle {
	@Override
    public void run() {
    	System.out.println("택시가 달립니다.")
    }
}

// 다형성 적용 - 매개변수
public class DriverExample {
	public static void main(String[] args) {
		// Driver 객체 생성
    	Driver driver = new Driver();

   		// 매개값으로 Bus 객체 생성 후, driver() 메소드 실행
	    Bus bus = new Bus();
    	driver.dirve(bus); // 버스가 달립니다.
   
		// 매개값으로 Taxi 객체 생성 후, driver() 메소드 실행
	    Taxi taxi = new Taxi();
    	driver.dirve(taxi); // 택시가 달립니다.
    }
}

객체 타입 확인 : instanceof

boolean result = 객체 instanceof 타입
public class Person {
	public String name;

    public Person(String name) {
    	this.name = name;
    }

	public void walk() {
    	System.out.println("걷습니다.")
    }
}

public class Student extends Person {
	public int studentNo;

    public Student(String name, int studentNo) {
    	super(name);
    	this.studentNo = studentNo;
    }

	public void study() {
    	System.out.println("공부합니다.")
    }
}

// 정적 메소드로 바로 적용 후 돌리기
public class InstanceofExample {
	public static void personInfo(Person person) {
    	System.out.println("name: " + person.name);
        person.walk();

        if(person instanceof Student student) {
        	System.out.println("studentNo: " + student.student.No);
            student.study();
        }
    }

    public static void main(String[] args) {
		// Person 객체 매개값으로 제공하고 personInfo() 메소드 호출
    	Person p1 = new Person("홍길동");
		personInfo(p1);
		// name: 홍길동
		// 걷습니다.
   
	    Person p2 = new Student("고길동", 10);
    	personInfo(p2);
		// name: 고길동
		// 걷습니다.
		// studentNo: 10
		// 공부합니다.
    }
}

추상 클래스

추상 클래스는 실체 클래스의 공통 필드와 메소드를 추출해서 만들어서 new 연산자 사용해서 객체 직접 생성 불가

// Animal은 추상 클래스
Animal animal = new Animal(); // 불가능

추상 클래스는 새로운 실체 클래스를 만들기 위한 부모 클래스로만 사용되기 떄문에 무조건 extends 뒤에만 와야 함

// 선언하는 방법
public abstract class 클래스명 {
	// 필드
    // 생성자
    // 메소드
}
  • 추상 클래스도 필드, 메소드 선언 가능
  • 자식 객체가 생성될 떄, super()로 추상 클래스의 생성자가 호출되므로 반드시 생성자가 있어야만 한다.
  • 추상 클래스 작성할 때, 추상 메소드를 작성할 수 있다.
public abstract class Animal {
	abstract void sound(); 
	// abstract 키워드 붙이고, 실행 중괄호 {}가 없음
	// 자식 메소드에서 오버라이딩해서 써야 함
}
// 추상 메소드 적용 예시
public abstract class Animal {
	// 메소드 선언
    public void breathe() {
    	System.out.println("숨을 쉽니다.");
    }

    // 추상 메소드 선언
    public abstract void sound();
}

public class Dog extends Animal {
	// 추상 메소드 오버라이딩
    @Override
    public void sound() [
    	System.out.println("멍멍");
    }
}

public class Cat extends Animal {
	// 추상 메소드 오버라이딩
    @Override
    public void sound() [
    	System.out.println("야옹");
    }
}

// 추상 메소드 예시
public class AbstractMethodExample {
	public static void main(String[] args) {
    	Dog dog = new dog;
        dog.sound();
        // 멍멍

        cat cat = new Cat();
        cat.sound();
        // 야옹

        // 매개변수의 다형성
        animalSound(new Dog());
        // Animal animal = new Dog(); : 즉, 자동 타입 변환
        // 멍멍

        animalSound(new Cat());
        // Animal animal = new Cat(); : 즉, 자동 타입 변환
        // 야옹
    }

    public static void animalSound(Animal animal) {
    	animal.sound();
	}
}

2) 인터페이스에서의 다형성(얘를 자주 쓰기)

다형성 = 자동 타입 변환 + 메소드 오버라이딩

갈아끼우기 예시

// 인터페이스 : RemoteControl(구현 메소드 : turnOn())
// 구현 클래스 : Television, Audio

RemoteControl rc = new Television();
rc.turnOn(); // 텔레비전에서 구현된 turnOn() 메소드 실행

rc = new Audio(); // Audio 객체로 교체해서 대입
// 굳이 새로운 인스턴스 생성할 필요가 없음
rc.turnOn(); // 오디오에서 구현된 turnOn() 메소드 실행

인터페이스의 상수와 추상 메소드

인터페이스에 선언된 필드는 모두 public static final 특성을 가지므로 인터페이스에서 안 붙여도 컴파일러 과정에서 쟤 다 붙음

public interface RemoteControl {
	int MAX_VOLUME = 10;
    int MIN_VOLUME = 0;
   
    void turnOn();
}

public class RemoteControlExample {
	public static void main(String[] args) {
    	System.out.println("리모콘 최대 볼륨: " + RemoteControl.MAX_VOLUME);
        // "리모콘 최대 볼륨: 10"
    }
}

이런 식으로 바로 인터페이스에 접근 가능

상속에서는 abstract를 꼭 붙여줘야 됐지만 인터페이스에서는 괜춘

public interface RemoteControl {
	int MAX_VOLUME = 10;
    int MIN_VOLUME = 0;

    void turnOn();
    // turnOn() 메소드에는 public abstract가 컴파일러 과정에 붙는다
}

인터페이스의 디폴드 메소드와 정적 메소드

추상 메소드는 메소드 실행부 중괄호가 없었지만, 디폴트 메소드는 실행부 존재

public interface RemoteControl {
	int MAX_VOLUME = 10;
    int MIN_VOLUME = 0;

    void turnOn();
    void turnOff();
    void setVolume(int volumn);
  
    default void setMute(boolean mute) {
    	if(mute) {
        	System.out.println("무음 처리");
            setVolume(0);
        } else {
        	System.out.println("무음 해제");
        }
	}
}

디폴트 메소드 호출할 때는 추상 메소드와 똑같이 인스턴스 객체 생성해서 해야함

RemoteControl rc = new Television();
rc.turnOn();
rc.setMute(true); // 텔레비전의 setMute()로 실행

rc = new Audio(); // Audio 객체로 교체해서 대입
rc.turnOn();
rc.setMute(true); // 오디오의 setMute()로 실행

정적 메소드 구현도 가능

public interface RemoteControl {
	int MAX_VOLUME = 10;
    int MIN_VOLUME = 0;

    void turnOn();
    void turnOff();
    void setVolume(int volumn);
  
    default void setMute(boolean mute) {
    	if(mute) {
        	System.out.println("무음 처리");
            setVolume(0);
        } else {
        	System.out.println("무음 해제");
        }
	}

    static void changeBattery() {
    	System.out.println("리모콘 건전지 교체");
    }
}
RemoteControl.changeBattery();
// 정적 메소드니까 인터페이스명으로 바로 접근해서 호출 가능

정적 메소드는 인터페이스 구현 클래스들의 중복 코드 구현할 때 응용하기

인터페이스의 타입 변환

자동 타입 변환의 비애 : 인터페이스에 구현된 메소드만 사용 가능

// RemoteControl 인터페이스
// turnOn(); turnOff(); setVolume(int volume):

// Television 클래스
// turnOn(); turnOff(); setVolume(int volume):
// setTime(); record();

RemoteControl rc = new Television();
rc.turnOn();
rc.turnOff();
rc.setVolume(5);
rc.setTime(); // 불가능
rc.record(); // 불가능

그래서 캐스팅을 활용한 강제 타입 변환을 행하기

// RemoteControl 인터페이스
// turnOn(); turnOff(); setVolume(int volume):

// Television 클래스
// turnOn(); turnOff(); setVolume(int volume):
// setTime(); record();

RemoteControl rc = new Television();
rc.turnOn();
rc.turnOff();
rc.setVolume(5);

Television tv = (Television) rc; // 강제 타입 변환
tv.setTime(); // 가능
tv.record(); // 가능

다형성 적용

2,3주차 리팩토링회고 참조하기


4. 예외 처리, 라이브러리, 모듈

예외(exception)와 에러(error)는 다르다.

  • 에러(error) : 컴퓨터 하드웨어의 고장으로 발생하는 응용프로그램 실행 오류
  • 예외(exception) : 잘못된 코드 사용으로 발생하는 오류

try, catch, finally

자바스크립트와 유사, 단 자바는 예외 처리 코드 역시 깐깐하게

  • 예외 처리에서도 객체가 생성된다. 이 객체는 예외 클래스로부터 생성된다.
  • 자바의 모든 에러와 예외 클래스는 Throwable 클래스를 상속받는다.
  • 또한, 예외 클래스는 java.lang.Exception 클래스를 상속받는다.

catch 블록에서의 인자 e를 통해서 예외 메시지를 출력할 수 있다.

public static void printLength(String data) {
    try {
        int length = data.length();
        System.out.println("문자 수: " + length);
    } catch (NullPointerException e) {
        System.out.println(e.getMessage());
        System.out.println(e); // 어떤 종류의 에러인지까지
        e.printStackTrace(); // 시뻘건 메시지들이 얘였네
        e // 그낭 e만 띄워도 똑같
    } finally {
        System.out.println("마무리");
    }
}

catch 블록이 여러 개여도 괜찮다. 단, 실행은 오로지 하나의 catch 블록만 실행된다. 이유는, try 블록에서 두 개 이상의 에러가 동시다발할 경우가 없기 떄문이다.

public static void main(String[] args) {
    String[] array = {"100", "1oo"};

    for(int i = 0; i<=array.length; i++) {
        try {
            int value = Integer.parseInt(array[i]);
            System.out.println("array[" + i + "]: " + value);
        } catch(ArrayIndexOutOfBoundsException e) {
            System.out.println("배열 인덱스 오버: " + e.getMessage());
        } catch(NumberFormatException e) {
            System.out.println("숫자 변환 불가: " + e.getMessage());
        }
    }
}

Exception : 클래스

Exception 은 상위 예외 클래스이다.
상위 예외 클래스는 언제나 아래쪽에 작성해주는 것이 원칙이다.

public static void main(String[] args) {
    String[] array = {"100", "1oo"};

    for(int i = 0; i<=array.length; i++) {
        try {
            int value = Integer.parseInt(array[i]);
            System.out.println("array[" + i + "]: " + value);
        } catch(ArrayIndexOutOfBoundsException e) {
            System.out.println("배열 인덱스 오버: " + e.getMessage());
        } catch(Exception e) {
            System.out.println("실행 문제 발생");
        } // 이렇게 말이죠
    }
}

만약 위쪽에 작성하게 되면 컴파일 에러를 발생시킨다.
이유는 이미 상위 클래스인 Exception 에 의해서 전부 걸러질 텐데
걸러진 예외를 다시 거르려고 하기 때문이다.

public static void main(String[] args) {
    String[] array = {"100", "1oo"};

    for(int i = 0; i<=array.length; i++) {
        try {
            int value = Integer.parseInt(array[i]);
            System.out.println("array[" + i + "]: " + value);
        } catch(Exception e) { // 이미 여기서 걸러지는 데
            System.out.println("실행 문제 발생");
        } // catch(ArrayIndexOutOfBoundsException e) {
          //   System.out.println("배열 인덱스 오버: " + e.getMessage());
       // } 굳이 여기서 한번 더 거를 이유가 없기 때문
    }
}

두 개 이상의 예외를 하나의 catch 블록으로 동일하게 예외 처리하려면
| 기호를 쓰면 된다

public static void main(String[] args) {
    String[] array = {"100", "1oo", null, "200"};

    for(int i = 0; i <= array.length; i++) {
        try {
            int value = Integer.parseInt(array[i]);
            System.out.println("array[" + i + "]: " + value);
        } catch(ArrayIndexOutOfBoundsException e) {
            System.out.println("배열 인덱스 초과: " + e.getMessage());
        } catch(NullPointerException | NumberFormatException e) {
            System.out.println("데이터 자체에 문제");
        } // NPE랑 NFE가 동시에 처리됨
    }
}

리소스(데이터 제공 객체)

리소스는 열었으면(open) 닫아야 한다(close).
하지만 컴퓨터는 멍충멍충해서 스스로 닫지 않는다.
닫는 방법으로 finally 블록에서 close()를 호출하는 경우가 있다.

FileInputStream fis = null;

try {
	fis = new FileInputStream("file.txt"); // 파일 열기
    // ...
} catch(IOException e) {
	// ...
} finally {
	fis.close(); // 파일 닫기
}

사실 try-with-resource 블록을 사용하면 예외 발생 여부와 상관없이 리소스를 자동으로 닫아줄 수 있다.
단, 리소스는 java.lang.AutoCloseable 인터페이스를 구현해서 close() 메소드를 재정의해야 한다.

// 리소스 Practice5
package parctice5;

public class Practice5 implements AutoCloseable {
    private String name;

    public Practice5(String name) {
        this.name = name;
        System.out.println("[MyResource(" + name + ") 열기]");
    }

    public String read1() {
        System.out.println("[MyResource(" + this.name + ") 읽기]");
        return "100";
    }

    public String read2() {
        System.out.println("[MyResource(" + this.name + ") 읽기]");
        return "abc";
    }

    @Override
    public void close() throws Exception {
        System.out.println("[MyResource(" + this.name + ") 닫기]");
    }
}

// 실행 예제
package parctice5;

public class TryWithResourceExample {
    public static void main(String[] args) {
    	// 첫 번째 케이스
        try (Practice5 res = new Practice5("A")) {
            String data = res.read1();
            int value = Integer.parseInt(data);
        } catch(Exception e) {
            System.out.println("예외 처리: " + e.getMessage());
        }

        System.out.println();

		// 두 번째 케이스
        try (Practice5 res = new Practice5("A")) {
            String data = res.read2();
            int value = Integer.parseInt(data); // NFE 발생
        } catch(Exception e) {
            System.out.println("예외 처리: " + e.getMessage());
        }

        System.out.println();

		// 세 번째 케이스
        Practice5 res1 = new Practice5("A");
        Practice5 res2 = new Practice5("B");

        try (res1; res2) { // 만약 리소스가 여러 개면 세미콜론으로 구분
            String data1 = res1.read1();
            String data2 = res2.read1();
        } catch(Exception e) {
            System.out.println("예외 처리: " + e.getMessage());
        }
    }
}

try 블록에서의 예외 발생 여부와 상관없이 안전하게 close() 메소드가 실행된다.

근데 마지막을 보면

[MyResource(A) 열기]
[MyResource(B) 열기]
[MyResource(A) 읽기]
[MyResource(B) 읽기]
[MyResource(B) 닫기]
[MyResource(A) 닫기]

이런 식으로 B가 먼저 닫히고 그다음에 A가 닫히는데, 그 이유는 닫히는 순서는 리소스가 열린 순서의 역순으로 이루어지기 때문이다.
JVM 내부적으로 스택 자료구조와 유사한 형태를 보인다.

예외 처리

예외는 떠넘길 수 있다.

public class Practice6 {
    public static void main(String[] args) {
        try {
            findClass();
        } catch(ClassNotFoundException e) {
            System.out.println("예외 처리: " + e);
        }
    }

    public static void findClass() throws ClassNotFoundException {
        Class.forName("java.lang.String2");
    } // 실제로 예외가 발생하는 위치는 findClass 메소드 내부
}

findClass 메소드가 ClassNotFoundException 을 선언하여 해당 예외를 직접 처리하지 않고 메소드 선언부에 throws ClassNotFoundException 를 사용하여 예외를 호출한 곳으로 넘기고 있음.
즉, main 메소드에서 예외 처리하는 방식으로 findClass 가 처리하는 방식이 대체, 따라가게 됨.

나열할 예외 클래스가 많다면, throws Exception 이나 throws Throwable 로 처리할 수 있음

public class Practice7 {
    public static void main(String[] args) throws Exception {
        findClass();
    } 
    // main 메소드가 떠넘긴 예외는 최종적으로 JVM에서 처리된다.
    // JVM은 예외의 내용을 콘솔에 출력하는 것으로 예외 처리를 한다.

    public static void findClass() throws ClassNotFoundException {
        Class.forName("java.lang.String2");
    }
}

사용자가 직접 예외를 정의할 수도 있다.
(예시 : 잔고 한도 초과 금액 인출시 발생시킬 예외 InsufficientException)

package practice8;

// 사용자 정의 예외
public class InsufficientException extends Exception {
    public InsufficientException() {
    } //  메시지를 지정하지 않고 예외를 던질 때 기본 생성자가 호출

    public InsufficientException(String message) {
        super(message);
    } // 예외 객체를 생성할 때 발생한 예외에 대한 설명이나 부가 정보를 포함하고자 할 때 사용
}

// 예시 클래스
package practice8;

public class Account {
    private long balance;

    public Account() {
    }

    public long getBalance() {
        return balance;
    }

    public void deposit(int money) {
        this.balance += money;
    }

    public void withdraw(int money) throws InsufficientException {
        if(this.balance < money) {
            throw new InsufficientException("잔고 부족: " + (money-this.balance) + " 부족");
        }
        this.balance -= money;
    }
}

// 사용례
package practice8;

public class AccountExample {
    public static void main(String[] args) {
        Account account = new Account();

        // 예금
        account.deposit(10_000);
        System.out.println("예금액: " + account.getBalance());

        // 출금
        try {
            account.withdraw(30_000);
        } catch (InsufficientException e) {
            System.out.println(e.getMessage());
        }
    }
}

여기서 예외 처리가 실제로 발생하는 곳은 withdraw 메소드이지만, 그 처리 방식은 main 메소드 내에서 처리하는 대로 따라가겠다는 뜻이다.
예외 객체에는 예외를 발생시킨 상황에 대한 부가 정보를 담아서 예외를 처리하는 곳에서 활용이 가능하다.

Record

이 둘은 같다

public class PersonDto {
    private final String name;
    private final int age;

    public PersonDto(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String name() {
        return name;
    }

    public int age() {
        return age;
    }

    @Override
    public int hashCode() {
        // ...
    }

    @Override
    public boolean equals(Object obj) {
        // ...
    }

    @Override
    public String toString() {
       // ...
    }
}
public record PeronDto(String name, int age) {
}

레코드 소스를 컴파일하면 변수 타입과 이름을 이용해서 private final 필드가 자동 생성되고, 생성자 및 Getter 메소드가 자동으로 추가된다. 그리고 hashCode(), eqauls(), toString() 메소드를 재정의한 코드도 자동으로 추가된다.

기타 더 공부해야 될 것들

어노테이션 써먹기


5. 제네릭

드디어 제네릭

제네릭 값의 치환에 따라서 해당 클래스 내부의 필드와 메소드의 타입이 제네릭 값이 됨으로써 다형성 실현 가능

간단 이해

박스 안에는 String, int, Object... 기타 등등 여러 가지가 들어갈 수 있는데, 이 모든 걸 전부 지정하는 건 노가다
그렇기 떄문에 결정되지 않은 타입타입 파라미터로 처리하고, 실제 사용할 때 대체, 제시하면 만사 오케이

  • 여담으로, 타입 파라미터를 대체할 때는 클래스 및 인터페이스만 가능
public class Box <T> {
	public T content;
}

Box<Integer> box = new Box<Integer>(); // ok
Box<int> box = new Box<int>(); // 안돼!

제네릭 메소드

public <A, B ...> 리턴타입 메소드명(매개변수, ...) {...}
// 여기서 <A, B ...> 에 해당하는 부분이 '타입 파라미터'

타입 파라미터를 통해 컴파일러가 메소드 호출 시, 타입 추론이 가능해지면서 저 메소드를 쓸 때 리턴값의 타입을 직접적으로 명시하지 않아도 된다.

public class GenericExample {

    // 제네릭 메소드
    public static <T> void printClassName(T value) {
        System.out.println("Class name: " + value.getClass().getSimpleName());
    }

    public static void main(String[] args) {
        // 다양한 타입을 사용하여 제네릭 메소드 호출
        printClassName("Hello");      // T는 String으로 추론됨
        printClassName(42);           // T는 Integer로 추론됨
        printClassName(3.14);         // T는 Double로 추론됨
        printClassName(new Person()); // T는 Person으로 추론됨
    }
}

물론, 명시적으로 리턴 타입을 지정하는 것도 가능

타입 파라미터

좀 더 타입 파라미터에 대해 이해하자면...
타입 파라미터는 클래스 혹은 메소드에서 쓰일 수 있으며, <T> 로 표현할 수 있겠다.
얘의 역할은 일종의 placeholder.

1) 클래스에서의 타입 파라미터

public class Box<T> { // Box 옆에 쓰인 <T>가 타입 파라미터
    public T content;

    public boolean compare(Box<T> otherContent) {
        boolean result = content.equals(otherContent.content);
        return result;
    }
}

사실 클래스에서의 타입 파라미터는 쉽다. 다만 타입 파라미터가 뭔지도 모르고 써왔으니 문제지.... 복습하라고 좀
다만, 이걸 외부 클래스에서 사용할 때 고려할 부분이 있는데...

  • 1번 케이스
public static void main(String[] args) {
    Box box1 = new Box();
    box1.content = "100";
	// ...
  • 2번 케이스
public static void main(String[] args) {
    Box<String> box1 = new Box<>();
    box1.content = "100";
	// ...

둘 다 사실 문법적으로 틀린 부분은 없다. 다만, 인텔리제이에서 작성을 하면 1번 케이스는 노란색 밑줄이 쭉 그인다. 문법적인 에러는 없음에도 경고를 주는 표식인데...

보면 경고가 있는데, Raw use of parameterized class 'Box' 라는 경고 문구는 Java에서 제네릭을 사용하는 클래스에 대해 타입을 명시하지 않고 사용했을 때 나타나는 경고다. 다만 프로그램을 돌리면 잘 작동되는 이유는, 타입 추론 때문에 그러하다.

public 필드인 content"100" 이라는 String으로 대입시키면서 제네릭 타입 클래스인 Box 에서 '아, 이 객체의 타입은 String` 이구나 하면서 추론을 하는 것이다.

다만, 원시 타입을 사용하고, 후에 특정 타입의 데이터로 지정하면서 객체의 타입을 추론하는 과정은 문법적으로는 에러가 없지만 컴파일러가 해당 클래스의 제네릭 타입 안전성을 확인할 수 없게 되며, 이는 코드에서 잠재적인 문제를 발생할 수 있음을 알려주는 사전 경고라고 볼 수 있겠다.

이런 식으로 타입을 명시하면 경고 라인이 사라지게 된다. 여담으로,

Box<String> box1 = new Box<String>();

이렇게 작성해도 똑같긴 하지만 Java 7 부터는 다이아몬드 연산자 <> 내부의 타입 인자를 생략할 수 있게 돼서 안 써도 되는데(인텔리제이에서도 무시시킴) 이를 다이아몬드 연산자 생략이라고 한다더라.

2) 메소드에서의 타입 파라미터

또 다른 패키지에 Box 클래스를 제네릭 타입으로 선언했다.

public class Box<T> {
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

간단한 gettersetter 가 있는 클래스인데, 외부에서 해당 클래스의 setter 를 직접 쓰는 게 귀찮(?)아서 아예 메소드를 만들어 활용해보기로 했다.

public class GenericExample {
	// 얘도 Box<T> 앞에 쓰인 <T>가 타입 파라미터
    public static <T> Box<T> boxing(T t) { 
        Box<T> box = new Box<>(); // 아까 위에서 봤던 타입 명시
        box.setT(t);
        return box;
    }

    public static void main(String[] args) {
		// ...

static인지 여부는 중요한 게 아니고, 이제 이 메소드를 써서 밑의 메인 메소드에서 활용해보자

  • 1번 케이스
public static void main(String[] args) {
	Box intBox = boxing(100);
    int intBoxValue = (int) intBox.getT(); 
	// 타입 추론 없이 Object 타입으로 지정돼서 강제 타입 캐스팅 필요
  • 2번 케이스
public static void main(String[] args) {
	Box<Integer> intBox = boxing(100);
    int intBoxValue = intBox.getT();

얘네도 둘 다 틀린 문법은 아니다. 다만 클래스의 케이스에서처럼 1번 케이스에서도 노란색 밑줄이 그어지며 경고가 뜬다.

역시나 제네릭을 사용하는 클래스에 대해 타입을 명시하지 않고 사용했을 때 나타나는 경고가 발생하게 된다. 또한 intBox는 Box intBox = boxing(100); 부분에서 제네릭 타입이 정해지지 않으면서 intBox 객체의 필드의 T 가 원시 타입인 Object 로 정해지게 되고 그 필드를 가져오는 getter 로 가져온 값을 (int)강제 캐스팅을 하면서, 선언된 int intBoxValue 에 담을 수 있게 되는 것이다.

여담으로 클래스 1번 케이스에서의 box1.content = "100"; 는 좌변은 원시 타입인 Object 이고, 우변은 String 인데, 묵시적 형변환이 이뤄지기 때문에, 제네릭과 관련된 경고와 별개로 문제가 없다. 메소드 1번 케이스에서는 좌변(int)이 우변(Object)보다 범위가 작기 때문에 명시적 형변환이 필요한 것이다.

다시 메소드의 2번 케이스로 돌아가서...

역시나 타입을 명시하면 강제 캐스팅을 할 필요도 없어지고, 노란색 줄이 없어지면서 보기에도 깔끔해진다(중요)

타입 파라미터 <T> 를 기재하지 않는 것은 해당 클래스 혹은 메소드에서 T가 뭔데...? 라는 반문을 받을 수 있는 상황인 것이다. 타입 파라미터를 기재해야 내가 해당 클래스 혹은 메소드에서 제네릭으로 타입을 선언해서 범용성을 넓히겠다는 최소한의 필수불가결 의도를 나타내는 셈이다.


계속 작성중...

profile
scientia est potentia / 벨로그 이사 예정...

0개의 댓글