sm 기술 블로그

객체지향 (Object /enum / 복제,참조 /제네릭) 본문

Java

객체지향 (Object /enum / 복제,참조 /제네릭)

sm_hope 2022. 4. 19. 15:45
Object
모든 클래스들의 조상

toString()

class Calculator{
    int left, right;
      
    public void setOprands(int left, int right){
        this.left = left;
        this.right = right;
    }
    public void sum(){
        System.out.println(this.left+this.right);
    }
      
    public void avg(){
        System.out.println((this.left+this.right)/2);
    }
}
  
public class CalculatorDemo {
      
    public static void main(String[] args) {
          
        Calculator c1 = new Calculator();
        c1.setOprands(10, 20);
        System.out.println(c1);
    }
  
}
//출력 결과
org.opentutorials.javatutorials.progenitor.Calculator@11be650f

패키지와, 메소드 의 내용이 나온다 (@ 이후는 의미 없음)

 

여기에. toString을 붙여

class Calculator{
    int left, right;
      
    public void setOprands(int left, int right){
        this.left = left;
        this.right = right;
    }
    public void sum(){
        System.out.println(this.left+this.right);
    }
      
    public void avg(){
        System.out.println((this.left+this.right)/2);
    }
}
  
public class CalculatorDemo {
      
    public static void main(String[] args) {
          
        Calculator c1 = new Calculator();
        c1.setOprands(10, 20);
        System.out.println(c1.toString());
    }
  
}

다음과 같이 변경해준다해도 출력 결과는 같다. (이미 object에서 toString을 자동적으로 붙여주기 때문에)

 

class Calculator{
    int left, right;
      
    public void setOprands(int left, int right){
        this.left = left;
        this.right = right;
    }
    public void sum(){
        System.out.println(this.left+this.right);
    }
      
    public void avg(){
        System.out.println((this.left+this.right)/2);
    }
    
    public String toString() {
    	return "left : " + this.left + ", right : " + this.right;
    }
}
  
public class Main {
      
    public static void main(String[] args) {
          
        Calculator c1 = new Calculator();
        c1.setOprands(10, 20);
        System.out.println(c1.toString());
    }
  
}

우리는 toString을 오버라이딩 하여 재정의 하였다. 결과를 사용자의 기호에 맞게 바꿀 수 있다.

만약 toString을 제거 한다 하더라도 출력 결과는 오버라이딩 한 대로 나올 것이다.

 

그런데 만약 toString의 기본 로직과 오버라이딩한 로직을 같이 출력하고 싶다면,

return super.toString() + ", left : " + this.left + ", right : " + this.right; 로 바꿔주면 된다.

출력 결과는 아래와 같다.

 

equals()

class Student{
	
    String name;
    Student(String name){
        this.name = name;
    }
    
    public boolean equals(Object obj) {
    	// object obj = s2 라는 과정 [자식이 부모행세 하는것은 가능]
    	// s2 자식 , object 부모
    	
        Student _obj = (Student)obj;
        // 기본적으로 부모가 자식의 행세를 하는것은 불가능하다
        // 부모는 없던거를 만드는것이기 때문에 불가능한 것
        // (Student)로 형변환을 하여 대입할 수 있다.
        // obj 부모 , Student 자식
        
        return name == _obj.name;
    }
}
 
class Main {
 
    public static void main(String[] args) {
        Student s1 = new Student("egoing");
        Student s2 = new Student("egoing");
        System.out.println(s1 == s2);
        System.out.println(s1.equals(s2));
        // 다형성으로 인해 Student(String name)에 접근하지 않고 equals 메소드에 접근한다.
        // 따라서 Student _obj = (String)obj;가 필요하다.
 
    }
 
}

자식 클래스를 부모 클래스에 할당한다. 그래서 Object obj에 들어가 있는 값은 s2고 데이터 타입을 Object로 하게 된다면, name이라는 변수에 접근할 수 없다.

그 이유는 Object는 name이라는 변수가 존재하지 않기 때문이다. -> 다형성!

 

Student _obj = (Student) obj; 이 구문을 통해 name에 접근 가능하도록 해준다.

 

hashCode()는 개인적으로 알아보자.

 

 

==을 쓰는 경우

 

원시 데이터 형(Primitive Data Type)이란 자바에서 기본적으로 제공하는 데이터 타입으로 byte, short, int, long, float, double, boolean, char가 있다.

이러한 데이터 타입들은 new 연산자를 이용해서 생성하지 않아도 사용될 수 있다는 특징이 있다.

 

 

finalize()

객체가 소멸될 때 호출되도록 약속된 메소드. 이 메소드는 여러 가지 이유로 개발자들은 finalize 않으려고 한다.

 

이것보다는 가비지 컬렉션(garbage collection)에 대해서 아는 것이 중요하다.

https://d2.naver.com/helloworld/1329

 

clone()

class Student implements Cloneable{
    String name;
    Student(String name){
        this.name = name;
    }
    protected Object clone() throws CloneNotSupportedException{
        return super.clone();
    }
}
 
class Main {
 
    public static void main(String[] args) {
        Student s1 = new Student("egoing");
        try {
            Student s2 = (Student)s1.clone();
            // 복제를 하려면 복제 가능한 객체라는 사실을 알려줘야한다.
            // Cloneable이라는 인터페이스를 생성해주면 된다.
            // Cloneable은 복제가능하다는 것을 알려주는 약속으로
            // Cloneable은 본문을 가지고 있지 않다. 
            
            System.out.println(s1.name);
            System.out.println(s2.name);
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
 
}

모든 클래스의 조상은 object이다.

그래서 기본적인 메소드를 가지고 있고 그것을 오버라이딩을 통해 사용자가 원하는 대로 사용할 수 있다.

 


enum

 

상수는 변하지 않는 값이다. 그래서 1=2는 성립할 수 없다.

 

public class Main {
 	
    private final static int APPLE = 1;
    private final static int PEACH = 2;
    private final static int BANANA = 3;
    
    
    public static void main(String[] args) {
        int type = APPLE;
        switch(type){
            case APPLE:
                System.out.println(57+" kcal");
                break;
            case PEACH:
                System.out.println(34+" kcal");
                break;
            case BANANA:
                System.out.println(93+" kcal");
                break;
        }
    }
}

자신뿐만 아니라 다른사람이 코드를 봤을 때 코드에 대한 이해를 쉽게 하기 위해서 상수를 문자로 사용하였다.

그래서 이름만 보면 대충 이해는 가능하다.

 

public class ConstantDemo {
    // fruit
    private final static int FRUIT_APPLE = 1;
    private final static int FRUIT_PEACH = 2;
    private final static int FRUIT_BANANA = 3;
     
    // company
    private final static int COMPANY_GOOGLE = 1;
    private final static int COMPANY_APPLE = 2;
    private final static int COMPANY_ORACLE = 3;
     
    public static void main(String[] args) {
        int type = FRUIT_APPLE;
        switch(type){
            case FRUIT_APPLE:
                System.out.println(57+" kcal");
                break;
            case FRUIT_PEACH:
                System.out.println(34+" kcal");
                break;
            case FRUIT_BANANA:
                System.out.println(93+" kcal");
                break;
        }
    }
}

 이런식으로 고유의 값을 지정할 수 있다. 

 

그런데 코드가 너무 지저분하다.... 정리해보자. 

interface FRUIT{
    int APPLE=1, PEACH=2, BANANA=3;
}
interface COMPANY{
    int GOOGLE=1, APPLE=2, ORACLE=3;
}
 // 위에서는 이름을 이용해서, 여기서는 문법을 이용해서 구분한 것이다.
 
public class ConstantDemo {
     
    public static void main(String[] args) {
        int type = FRUIT.APPLE;
        switch(type){
            case FRUIT.APPLE:
                System.out.println(57+" kcal");
                break;
            case FRUIT.PEACH:
                System.out.println(34+" kcal");
                break;
            case FRUIT.BANANA:
                System.out.println(93+" kcal");
                break;
        }
    }
}

여기에서 interface 에서 필드를 작성한다는 것은 public final static int이 포함되어 있음을 암시하는 것이다.

 

하지만 위에 코드는 단점이 있다. 과일 애플과 기업 애플이 가지고 있는 숫자가 같다면 이 둘은 같다고 판단할 것이다.

이러한 문제를 처리할 필요가 있다.

 

유용한 팁 -> ctrl+우측 클릭+리 펙터 링+리네임을 하면 한 번에 이름을 변경할 수 있다.

 

class Fruit{
    public static final Fruit APPLE  = new Fruit();
    public static final Fruit PEACH  = new Fruit();
    public static final Fruit BANANA = new Fruit();
}
class Company{
    public static final Company GOOGLE = new Company();
    public static final Company APPLE = new Company();
    public static final Company ORACLE = new COMPANY(Company);
}
 
public class ConstantDemo {
     
    public static void main(String[] args) {
        if(Fruit.APPLE == Company.APPLE){
            System.out.println("과일 애플과 회사 애플이 같다.");
        }
    }
}

이렇게 데이터 타입을 다르게 만들어 비교 자체를 불가능하게 만들 수 있다.

하지만 class 데이터 타입의 상수는 switch에서 사용할 수 없다. 이걸 enum을 통해서 해결해보자.

 

enum

배열은 서로 연관된 값들의 집합이라면 enum 즉, 열거형은 서로 연관된 상수들의 집합이다.

 

위에 지저분한 코드를 enum을 이용한다면

enum Fruit{
	//Fruit도 클래스이다.
    
    APPLE, PEACH, BANANA;
    
  	//public ~~~ 과 같은 뜻이다.
    //public static~~~ 은 많이 사용하기 때문에 간소화하기위해 만든것
}
enum Company{
    GOOGLE, APPLE, ORACLE;
}
 
public class ConstantDemo {
     
    public static void main(String[] args) {
        /*
        if(Fruit.APPLE == Company.APPLE){
            System.out.println("과일 애플과 회사 애플이 같다.");
        }
        */
        Fruit type = Fruit.APPLE;
        switch(type){
            case APPLE:
            	// APPLE 레이블
                System.out.println(57+" kcal");
                break;
            case PEACH:
            	// PEACH 레이블
                System.out.println(34+" kcal");
                break;
            case BANANA:
            	// BANANA 레이블
                System.out.println(93+" kcal");
                break;
        }
    }
}

다음과 같이 깔끔하게 바꿀 수 있다.

 

이렇게 되면 type은 에러를 표출하지 않을 것이다.

또한 Fruit.APPLE이 아닌 APPLE이라고 해야 에러가 없어질 것이고, 이것은 코딩을 더 간결하게 만든다.

 

그래서 enum의 사용 이유는 

  • 코드가 단순해진다.
  • 인스턴스 생성과 상속을 방지한다.
  • 키워드 enum을 사용하기 때문에 구현의 의도가 열거임을 분명하게 나타낼 수 있다.

enum과 생성자

enum Fruit{
    APPLE("red"), PEACH("pink"), BANANA("yellow");
	//()는 생성자를 호출한는 것을 의미하고 컬러값을 대입 시킴
	// enum안에는 메소드도,변수도 생성할 수 있다.
	
    private String color;
    
    // private로 color를 무단으로 변경하는 것을 막을 수 있음
    public String getColor() {
    	return this.color;
    }
    
    Fruit(String color){
        System.out.println("Call Constructor "+this);
        this.color = color;
    }
}
 
enum Company{
    GOOGLE, APPLE, ORACLE;
}
 
public class Main {
     
    public static void main(String[] args) {
        /*
        if(Fruit.APPLE == Company.APPLE){
            System.out.println("과일 애플과 회사 애플이 같다.");
        }
        */
        Fruit type = Fruit.APPLE;
        switch(type){
            case APPLE:
                System.out.println(57+" kcal, "+Fruit.APPLE.getColor());
                // 뒷 부분 Fruit.APPLE.getColor()는 red를 표시한다. 
                
                break;
            case PEACH:
                System.out.println(34+" kcal, "+Fruit.PEACH.getColor());
                break;
            case BANANA:
                System.out.println(93+" kcal, "+Fruit.BANANA.getColor());
                break;
        }
    }
}
// 출력 결과
Call Constructor APPLE
Call Constructor PEACH
Call Constructor BANANA
57 kcal

상수들의 값이 인스턴스화 될 때마다 생성자를 호출하여 세 번의 call constructor을 호출할 것이다.

 

class Fruit{
    public static final Fruit APPLE  = new Fruit();
    public static final Fruit PEACH  = new Fruit();
    public static final Fruit BANANA = new Fruit();
}
enum Fruit{
    APPLE("red"), PEACH("pink"), BANANA("yellow");
    private String color;
    
    public String getColor() {
    	return this.color;
    }
    
    Fruit(String color){
        System.out.println("Call Constructor "+this);
        this.color = color;
    }
}

두 방법의 가장 큰 차이는 클래스로 상수를 정의하게 되면 각각의 멤버(apple peach banana)를 배열처럼 열거할 수 없다.

 

열거형이 클래스보다 좋은 점은 열거형의 어떠한 데이터가 있는지 몰라도 마치 배열처럼 열거형의 데이터를 하나씩 꺼내서 사용할 수 있다.

 

그래서 다음과 같이 쓸 수 있다.

enum Fruit{
    APPLE("red"), PEACH("pink"), BANANA("yellow");
    private String color;
    Fruit(String color){
        System.out.println("Call Constructor "+this);
        this.color = color;
    }
    String getColor(){
        return this.color;
    }
}
 
enum Company{
    GOOGLE, APPLE, ORACLE;
}
 
public class Main {
     
    public static void main(String[] args) {
        for(Fruit f : Fruit.values()){
        	//values를 통해 Fruit의 데이터들을 하나씩 꺼내서 f에 담는다.
        	
            System.out.println(f+", "+f.getColor());
        }
    }
}
//출력결과
Call Constructor APPLE
Call Constructor PEACH
Call Constructor BANANA
APPLE, red
PEACH, pink
BANANA, yellow

참조(reference)

new로 생성하는 자료들은 참조 자료형이다.

 

복제

public class ReferenceDemo1 {
 
    public static void runValue(){
        int a = 1;
        int b = a;
        b = 2;
        System.out.println("runValue, "+a); 
    }
 
    public static void main(String[] args) {
        runValue();
    }
 
}

a의 값을 b에 복제하고 b를 2로 바꿨을 때 a의 값은 어떻게 될 것인가?

당연히 a는 1이다.

 

 

참조

class A{
    public int id;
    A(int id){
        this.id = id;
    }
}

public class Main {
 
    public static void runValue(){
        int a = 1;
        int b = a;
        // 변수 a에담겨있는 1을 복제해서 b에 넣는다는 뜻.
        
        b = 2;
        System.out.println("runValue, "+a); 
    }
     
    public static void runReference(){
        A a = new A(1);
        // 변수 a는 클래스 A의 인스턴스 주소값을 가지고 있는거
        A b = a;
        // A 타입인 b에 인스턴스화한 a를 담는다.
        // 변수 a가 가지고있는 클래스 A의 인스턴스 주소값을 b에 넣는거
        
        b.id = 2;
        // b를 2로 변경했을때? -> 2로 바뀐다.        
        System.out.println("runReference, "+a.id);      
    }
    // 기본데이터 타입이 담겨있는 1은 b에 복제가 되지만
    
    
    public static void main(String[] args) {
        runValue();
        runReference();
    }
 
}
//출력결과
runValue, 1
runReference, 2

복제와 참조

복제와 참조에 대한 예시는 아래 동영상을 참고하자.

https://www.youtube.com/watch?v=5XclqQAviyU&list=PLuHgQVnccGMCeAy-2-llhw3nWoQKUvQck&index=144 

복제는 파일을 복사 붙여놓기, 참조는 파일을 바로가기한 것과 같다 라는 내용.

 

메소드와 매개변수의 참조

class A{
    public int id;
    A(int id){
        this.id = id;
    }
}

public class Main {
     
    static void _value(int b){
        // 메소드에 매개변수로 b를 받는다 
    	// 이건 int b=a; 와 같은 말이다.
    	
    	b = 2;
    }
     
    public static void runValue(){
        int a = 1;
        // a라고 하는 변수에 1이 들어 있다. (기본 데이터타입)
        _value(a);
        System.out.println("runValue, "+a);
    }
     
    static void _reference1(A b){
        b = new A(2);
        // 변수 b가 만들어 졌는데 변수 b는 a가 참조하는 1과 다른 2를 참조하고 있다.
    }
     
    public static void runReference1(){
        A a = new A(1);
        _reference1(a);
        System.out.println("runReference1, "+a.id);     
    }
     
    static void _reference2(A b){
        b.id = 2;
        // 변수 b는 a가 가지고 있는 주소를 똑같이 가지고 있고 b가 2를 대입하면
        // b,a가 같은 인스턴스 주소를 참조하기 때문에 2를 출력한다.
    }
 
    public static void runReference2(){
        A a = new A(1);
        _reference2(a);
        System.out.println("runReference2, "+a.id);     
    }
     
    public static void main(String[] args) {
        runValue(); // runValue, 1
        runReference1(); // runReference1, 1
        runReference2(); // runReference2, 2
    }
 
}
//출력결과
runValue, 1
runReference1, 1
runReference2, 2

제네릭

제네릭의 구조

P1의 데이터 타입은 Person <String>

P2의 데이터 타입은 Person <StringBuilder>

 

제네릭을 왜 쓸까?

class StudentInfo{
    public int grade;
    StudentInfo(int grade){ this.grade = grade; }
}
class StudentPerson{
    public StudentInfo info;
    StudentPerson(StudentInfo info){ this.info = info; }
}
class EmployeeInfo{
    public int rank;
    EmployeeInfo(int rank){ this.rank = rank; }
}
class EmployeePerson{
    public EmployeeInfo info;
    EmployeePerson(EmployeeInfo info){ this.info = info; }
}
public class Main {
    public static void main(String[] args) {
        StudentInfo si = new StudentInfo(2);
        // 성적이 2를 가지고 있음
        
        StudentPerson sp = new StudentPerson(si);
        System.out.println(sp.info.grade); // 2
        
        EmployeeInfo ei = new EmployeeInfo(1);
        EmployeePerson ep = new EmployeePerson(ei);
        
        System.out.println(ep.info.rank); // 1
    }
}

2와 1이 출력되는 코드이다.

여기서 class StudentPerson와 class EmployeePerson의 로직이 같아 사실상 코드의 중복이 발생했다.

 

그러면 중복을 제거해 보자.

class StudentInfo{
    public int grade;
    StudentInfo(int grade){ this.grade = grade; }
}
class EmployeeInfo{
    public int rank;
    EmployeeInfo(int rank){ this.rank = rank; }
}
class Person{
    public Object info;
    Person(Object info){ this.info = info; }
} // 제네릭
// Object를 통해 Person() 괄호안에 모든 타입이 들어갈 수 있다.
// 이건 자바에서 허용할 수 없다.
// 이것을 타입이 안정하지 않다고 한다.

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("부장");
        EmployeeInfo ei = (EmployeeInfo)p1.info;
        System.out.println(ei.rank);
    }
}

제네릭을 통해 중복 제거는 성공했으나 Object를 통해 모든 데이터 타입을 허용해버렸다.

 

 

제네릭의 특성

class EmployeeInfo{
    public int rank;
    EmployeeInfo(int rank){ this.rank = rank; }
}
class Person<T, S>{
    public T info;
    public S id;
    Person(T info, S id){ 
        this.info = info; 
        this.id = id;
    }
}
public class Main {
    public static void main(String[] args) {
    	Integer id = 1;
    	Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(new EmployeeInfo(1), id);
    	System.out.println(p1.id.intValue());
    }
    // 복수의 제네릭이 필요할 때는 이름이 다르고 ,로 구분한다.
    // int, double 등 은 그냥 쓰면 제네릭으로 사용이 불가능하다.
    // 이걸 Integer, Double로 Wrapped 하여 사용할 수 있다. 
        
}

위와 같이 코드를 쓸 수 있지만 더 간결하게 할 수 있다.

그 방법은

https://www.youtube.com/watch?v=MhUb5itcJvk&list=PLuHgQVnccGMCeAy-2-llhw3nWoQKUvQck&index=149 

를 보자.

 

interface Info{
    int getLevel();
}
class EmployeeInfo implements Info{
    public int rank;
    EmployeeInfo(int rank){ this.rank = rank; }
    public int getLevel(){
        return this.rank;
    }
}
class Person<T extends Info>{
    public T info;
    Person(T info){ this.info = info; }
}
public class GenericDemo {
    public static void main(String[] args) {
        Person p1 = new Person(new EmployeeInfo(1));
        Person<String> p2 = new Person<String>("부장");
    }
}

interface를 사용하면 implement를 쓰지만 제네릭에서 extends는 부모가 누구다 라는 것을 알려주는 것이다.

Comments