ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TIL #17 - 추상, enum, interface
    프로그래밍/TIL(국비과정) 2020. 4. 18. 13:33

    # 달력만들기 

    사용자로부터 년도와, 월을 입력받아 그에 맞는 달력을 출력하는 프로그램이다. 

    월마다 끝나는 달이 다르고 시작하는 일의 요일도 다르다. 

    하지만 그에 맞게 사용할 수 있는 클래스와 메소드가 있어서 그걸 사용해주면 된다. 

     

    import java.util.Scanner;
    import java.util.Calendar;
    import java.util.Date;
    
    class CalendarTest{
    	private int year, month, week, lastDay; 
    
    	public CalendarTest(){
    		Scanner scan = new Scanner(System.in);
    		System.out.print("년도 입력 : ");
    		year = scan.nextInt();
    		System.out.print("월입력 입력 : ");
    		month = scan.nextInt();
    	}

    먼저 생성자를 통해서 입력을 받아주었다. 

    setter를 사용해도 된다. 

    필드로는 년도 year, 월 month, 요일 week, 월의 마지막 일 lastDay 가 있다. 

     

    해당 년도와 월에 따른 달력 내용은 find 메소드에서 찾는다. 

    public void find(){
    		Calendar cal = Calendar.getInstance();

    Calendar는 추상클래스이기 때문에 new를 사용해서 생성해줄 수 없기 때문에 Calendar를 생성해주는 코드는 아래와 같다. getInstance 안에서 Calendar를 만들어 받는다. 

    new가 아닌 메소드를 이용해 클래스를 생성하는 방법이다. 

    그런 후 년, 월을 입력받은 값으로 설정한다. 

    cal.set(Calendar.YEAR, year); 
    cal.set(Calendar.MONTH, month-1);

      month를 -1 하는 이유는 예를 들어 사용자로부터 4월을 입력받으면 Calendar.Month에게는 3월로 입력을 받는다. 

    Calendar.Month는 0부터 시작하기 때문이다. 그렇기 때문에 출력을 할 때는(사용자에게 보여줄 때는 ) +1 이고 

    입력을 받을 때는 (프로그램에 넣을 때는) -1을 해준다. 

    위와 같은 작업을 해주어야 시스템에서 년, 월을 시스템상의 년, 월로 인지하지 않는다. 

    일도 마찬가지로 위와 같은 작업을 해준다. 

    달력이기 때문에 무조건 1로 시작하게 설정해준다. 

    cal.set(Calendar.DAY_OF_MONTH, 1);

    위의 작업을 해주지 않으면 시스템상의 일을 출력한다. 

    cal.set으로 년, 월, 일을 한 번에 설정해줄 수도 있다. 

    cal.set(year, month-1, 1) 

     

    이렇게 년, 월, 일을 지정했으니 그에 따라 무슨 요일부터 1일이 시작되는지를 알아야한다. 

    그 작업은 아래와 같이 가능하다. 

    week = cal.get(Calendar.DAY_OF_WEEK); 

    또 선택한 월의 가장 큰 날(30, 31, 28, 29) 을 알려주는 코드는 아래와 같다. 

    lastDay = cal.getActualMaximum(Calendar.DAY_OF_MONTH);

     

    이렇게 각각 년월일을 알아냈다면 출력을 한다. 

    display() 메소드이다. 

    public void display(){
    		System.out.println("일\t월\t화\t수\t목\t금\t토");
    		for(int i = 1; i < week; i++){ 
    			System.out.print("\t");
                }

    먼저 1부터 시작하는데, 요일이 시작하는 일보다 작은 경우엔 탭을 적용한다. 

    만약 1일이 목요일부터 시작하면 일, 월, 화, 수 가 비게 되는데 week에서는 목요일을 5로 인지한다. 

    i는 week보다 작은 동안 탭이 적용되고 week보다 커지는 때에 탭을 멈추고 다음 for문으로 넘어가 본격적으로 일을 출력하게 된다. 

    for(int i = 1; i <= lastDay; i++){
    			System.out.print(i+"\t");
    			if(week % 7 == 0){
    				System.out.println();
    			}// if
    			week++;
    		}// for
         }
    }

    안쪽 for문에서 본격적으로 일을 찍게 되는데 

    i는 1부터 마지막 일인 lastDay까지 진행한다. 

    그런데 일주일이 지나면 일은 줄바꿈이 이루어진다. 

    그렇기 때문에 week가 7의 배수일 때 줄바꿈이 이루어지게 한다. 

    if문에서 나간 후엔 week++를 한다

     

    메소드 작업이 끝나면 메인 메소드에서 불러주면 된다. 

    class CalendarAgain {
    	public static void main(String[] args) {
    		CalendarTest ct = new CalendarTest();
    		ct.find();
    		ct.display();
    
    	}
    }

     

    #enum

    enum은 상수의 집합이다. 

    클래스를 선언하는 방식과 비슷한데 class의 자리에 enum을 넣어주면 된다. 

    클래스가 아닌 자료형이다. 

    (어떤 책에는 클래스의 일종이라고 하는데, 어느 것이 사실인지 잘 모르겠다. 추가 바람)

    enum EnumColor {
    	RED, GREEAN, BLUE, MAGENTA; 
    }

    EnumColor 라는 이름으로 상수 RED, GREEN, BLUE, MAGENTA를 넣었다. 

     이 enum을 어떻게 부르고 사용하는지도 보자. 

    class EnumMain{
    	String colorName;
    	EnumColor color;
    
    	public static void main(String[] args) {
    		System.out.println(EnumColor.RED);
    		System.out.println();

    enum은 모두 내부적으로 static이 설정되어있다. 

    그렇기 때문에 EnumColor 안의 색들을 하나의 값들로 인식이 되어 들어온다. 

    위와 같이 EnumColor.RED 로 RED를 출력해 낼 수 있다. 

     

    EnumMain em = new EnumMain();
    em.colorName = "빨강";
    em.color = EnumColor.RED;
    System.out.println(em.colorName + "\t" + em.color);

    위와 같은 방식도 가능하다. 

    Enum을 클래스식으로 불러와 colorName 변수에 "빨강"을 넣고 

    color에 EnumColor에 있는 RED 값을 넣어 각각 출력이 가능하다. 

     

    em.colorName = "보라";
    EnumColor VIOLET = EnumColor.MAGENTA;
    System.out.println(em.colorName + "\t" + VIOLET);
    System.out.println();

    위와 같이 하면, colorName에 보라라는 값이 들어간다. 

    int a = 25; 

    int b = 25; 를 했을 때 b에 a의 값 25가 들어가는 것과 같다. 

    그리고 MAGENTA와 VIOLET 상수가 같다는 것으로 표시된다. 

    따라서 같은 보라가 출력된다. 

    클래스만 자료형이 아닌 EnumColor 또한 그렇게 될 수있다는 것을 의미한다. 

     

    또 eNum의 집합은 인덱스 값이 0부터 시작한다. 

    for(EnumColor data : EnumColor.values()){
    			System.out.println(data + "\t" 
    				+ data.ordinal() + "\t"
    				+ data.valueOf(data + "")); // String으로 바꿔라 
    				// String값을 enum에서 찾는 valueOf
    		
            		// data.valueOf("BLACK"); 시 찾는 값이 없다는 에러가 뜸 (아래 설명 참조)
    		}

     

    ordinal()은 정의된 순서를 리턴하고 valudOf는 enum 요소를 순서대로 enum 타입 배열로 리턴한다. 

    출력된 ordinal을 보면 0부터 시작하는 것을 확인할 수 있다. 

     

    #Object

    object는 모든 클래스의 최상위 클래스다 

    우리가 보통 class 클래스명 {} 이라고 클래스를 만들 때 extends Object가 생략되어있는 꼴이다. 

    class Test {
    	@Override
    	public String toString(){
    		return getClass() + "@안녕";
    		// 오버라이드로 인해 주소값이 안녕으로 바뀜 
    	}
    }

    Test 클래스에 toString을 오버라이드 했다. 

    toString은 원래 클래스명@ 16진수 주소값을 보여주는 메소드이다. 

    그런데 Test 클래스에서 Obejct 클래스의 toString을 오버라이드해 클래스명@안녕이 리턴되게 만들었다. 

    class Sample{
    	
    }
    

    아무것도 없는 Sample 클래스도 만들었다. 

     

    이제 메인 메소드가 있는 ObejctMain에서 저 둘을 불러와본다. 

    class ObjectMain{
    	public static void main(String[] args) {
    		Test t = new Test();
    		System.out.println("객체 t = " + t); // 객체 t = Test@15db9742
    		System.out.println("객체 t = " + t.toString()); 
    

    Test 클래스를 불러와 객체 t의 주소값을 출력해본다. 

    결과는 아래와 같다 

    결과가 클래스명@16진수 주소값이 아닌 클래스명@안녕 이 나왔다. 

    클래스명@안녕은 Test에서 오버라이드한 toString 메소드이다. 

    Obejct는 최상위 클래스로 모든 클래스에 상속한다. 그러므로 모든 클래스의 부모클래스인 것이다. 

    Test는 Obect 클래스의 자식클래스이다. 

    자식클래스에서 부모클래스의 메소드를 오버라이드 하면 오버라이드 발동 우선권은 자식이 가지게 된다. 

    따라서 부모의 toString 메소드가 아닌 자식 (자기자신)의 toString 메소드가 발동되는 것이다. 

     

    		Sample s = new Sample();
    		System.out.println("객체 s = " + s); // 객체 s = Sample@15db9742
    		System.out.println("객체 s = " + s.toString()); 
    		System.out.println("객체 s = " + s.hashCode()); 
    		System.out.println();

    그럼 오버라이드를 하지 않은 Sample의 경우는 어떻게 나올까? 

    Sample 클래스의 경우 오버라이드를 하지 않았기 때문에 Object 클래스의 toString 메소드 결과 그대로 나온다. 

    hashCode()는 문자열을 10진수로 바꿔주는 것이다. 

    그런데 문자열의 갯수는 수없이 많다. 따라서 문자열을 10진수로 변환하는 건 어렵다. 

    int형은 21억개가 한계이고 문자열은 그 보다 많기 때문이다. 

    그렇기 때문에 hashCode의 결과를 그대로 믿지 않는 것이 좋다고 한다.. 

    (위의 경우엔 16진수 주소값을 10진수로 바꾼 것이기 때문에 괜찮다)

     

    다음은 apple이라는 문자열을 선언한 후 그에 따른 주소값을 본다.

    String str = "apple";
    		System.out.println("객체 str = " + str);
    		System.out.println("객체 str = " + str.hashCode());
    		System.out.println();

    분명 str을 출력하면 주소값이 출력될 텐데 아래의 결과를 보면 그렇지 않다. 

    결과를 보니 주소값이 아닌 apple이 출력된다. 

    이 이유는 오버라이드 때문이다. 

    Object의 주소값이 나오는 toString을 String 클래스에서 문자열이 나오게 오버라이드를 된 꼴이 된다. 

    (왜 그런지 잘 모르겠으므로 추후 보충하겠음)

     

     

    		Object cc = new Object();
    		Object dd = new Object();
    		System.out.println("cc == dd : " + (cc == dd) ); //f
    		System.out.println("cc.equals(dd) : " + cc.equals(dd)); //f - 참조값 비교 
    		System.out.println();
    
    		Object ee = new String("apple");
    		Object ff = new String("apple");
    		System.out.println("ee==ff : " + (ee == ff)); // f
    		System.out.println("ee.equals(ff) : " + ee.equals(ff)); // t - 문자열 비교 
    

    각각 Object cc, dd, ee, ff 를 만들었다. 

    그림으로 그리면 아래와 같다 

      먼저 cc와 dd를 비교해본다. 

    먼저 주소값을 비교해보고 그 다음은 문자열(참조값)을 비교해 본다. 

    		Object cc = new Object();
    		Object dd = new Object();
    		System.out.println("cc == dd : " + (cc == dd) ); //f
    		System.out.println("cc.equals(dd) : " + cc.equals(dd)); //f - 참조값 비교 
    		System.out.println();

      주소값의 경우에는 False가 나온다. 

    new로 생성을 했기 때문에 위의 그림과 같이 각자의 주소값을 가지게 되기 때문이다. 

    또 참조값의 경우에도 각자 다른 주소값을 가지고 있고, 그에 따라 참조하는 값도 다르기 때문에 false가 나오게 된다. 

     

    다음은 ee ff이다. 

    		Object ee = new String("apple");
    		Object ff = new String("apple");
    		System.out.println("ee==ff : " + (ee == ff)); // f
    		System.out.println("ee.equals(ff) : " + ee.equals(ff)); // t - 문자열 비교 
    

     

      ee와 ff는 apple이라는 같은 문자열의 참조를 하고 있다. 

    먼저 주소값의 경우에는 역시 각자 new를 했기 때문에 다르다. 결과는 false이다. 

    equals의 경우에는 true가 나온다. 

    위의 그림을 보면 각자 Stirng 자식 클래스를 가지고 있으면서 Object를 가리키고 있는 형태가 된다. 

    즉 생성은 자식으로 하고 참조는 부모로 하고있는 형태인 것이다. 

    하지만 equals를 발동했을 때 발동권은 자식인 자기자신에게 우선적으로 적용된다. 

    따라서 각자 본인이 가지고 있는 apple 이라는 문자열을 비교하게되어 true라는 결과가 도출된다. 

     

    # interface

    인터페이스는 아래 점프 투 자바의 설명을 참고하도록 한다. 

    https://wikidocs.net/217

     

    위키독스

    온라인 책을 제작 공유하는 플랫폼 서비스

    wikidocs.net

     

    interface의 기능을 보니 상속과 굉장히 비슷해 보였다. 

    lion, tiger와 같은 동물들을 ,predator 육식동물이라는 하나의 interface로 묶어 관리하는데, 

    이는 상속과도 굉장히 비슷하게 느껴졌다. 

    그래서 찾아보니 상속과 interface의 차이점은 강제성에 대한 것이었다. 

    상속의 경우에는 상속을 받더라고 하더라도 부모 클래스나 부모 메소들르 사용하지 않아도 오류가 뜨지 않는다. 

    하지만 interface의 경우에는 implements를 통해 어떤 interface를 받을 경우 그 interface를 사용하지 않으면 오류가 난다. 

    이와 같이 기능은 비슷하지만 사용에 대해 강제 하느냐 안하느냐에 대해 차이가 있다. 

     

    위의 설명에서 보았듯이 interface를 사용하기 위해서는 implements 키워드를 사용한다. 

    만약 상속 extends와 implements를 함께 사용한다면 implements는 제일 뒤로 사용한다. 

    아래와 같은 형태이다. 

    class Yang extends Choi implements Kang 

     

     

    이제 interface A 와 interface B 를 만들어 어떻게 사용하는지 코드를 통해 보자. 

    먼저 interface A 이다. 

    interface InterA {
    	public static final String NAME = "양아무개"; // 인터페이스에는 반드시 상수만 
    	int AGE = 25; // 상수임(static final 생략가능) 
    
    	public abstract void aa(); // 추상메소드 
    	public void bb(); // 추상매소드(abstract 생략가능) 
    
    	//public void cc(){} 구현부 불가 
    }

    interface의 경우에는 반드시 상수만 정의할 수 있다. 

    상수 정의시 필요한 static final은 생략할 수 있다. 

    또한, 메소드를 정의할 때도 몸통은 해당 인터페이스를 implements 한 클래스에서 구현해야하기 때문에 

    인터페이스에는 몸통없는 추상메소드만 정의할 수 있다. 

    단, abstract 키워드는 생략할 수 있다. 

    여기까지 하면 하나의 틀을 만든 셈이다. 

     

    interface InterB {
    	public void cc();
    	public void dd();
    }

    다음은 Interface B이다. 

    cc, dd  추상메소드를 만들었다. 

    interface InterC extends InterA, InterB {
    	// 다중 상속 
    
    }
    

    interface c의 경우에는 interA와 interB를 상속하였다. 

    원래 자바에서는 다중상속이 불가능하다. 

    그런데 위의 코드를 보면 interface이다. 

    인터페이스끼리의 다중상속은 가능하다. 

    정리하면 아래와 같다. 

    class 클래스 extends 클래스, 클래스 >> 불가능

    interface 인터페이스 extends 인터페이스, 인터페이스 >> 가능

     

    본격적으로 Main 클래스로가 각 인터페이스를 어떻게 사용하는지 본다. 

    InterMain이라는 클래스는 InterA와 InterB를 인터페이스 할 것이다. 

    그런데 InterC가 InterA와 InterB를 상속했기 때문에 아래와 같은 코드가 나온다. 

    class InterMain implements InterC { 

    아래의 코드와 같은 것이다.

    class InterMain implements InterA, InterB{

     

    implements를 한 클래스는 반드시 해당 인터페이스에 선언된 추상메소드를 override 해주어야한다. 

    위에서 말한 듯이 하지 않을 경우, 오류가 난다. 

    interface는 여러개 사용할 수 있지만 그 대신 또 그 인터페이스에 있는 메소드들을 오버라이드 해주어야한다. 

     

    만약, 오버라이드하기 싫어허 클래스 앞에 abstract를 붙이면 내가 아닌 내 다음 세대로 미루게 된다. 

    그렇게 되면 InterMain은 추상클래스는 new로 생성해줄 수 없다는 규칙에 의해 절대로 new할 수 없다. 

    	
    	// InterA에 대한 오버라이드 
    	public void aa(){}
    	public void bb(){}
    
    	// InterB에 대한 오버라이드 
    	public void cc(){}
    	public void dd(){}
    	
    	public static void main(String[] args) {
    		
    	}
    }

     

    댓글

Designed by Tistory.