스물일곱 번째 수업

정혅·2024년 3월 19일

더 조은 아카데미

목록 보기
32/76

오전문제

  1. 다음과 같은 실행 결과가 나오도록 코딩하시오.
    class Point3D {

    int x,y,z;
    Point3D(int x, int y, int z) {

    this.x=x;
    this.y=y;
    this.z=z;
    }
    Point3D() {

    this(0,0,0);
    }
    }

    // 실행 결과
    [1, 2, 3][1, 2, 3]
    p1==p2?false
    p1.equals(p2)?true

package com.test.memo;

class Point3D {
    int x, y, z;

    Point3D(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    Point3D() {
        this(0, 0, 0);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Point3D temp) {
            temp = (Point3D) obj;
            return x == temp.x && y == temp.y && z == temp.z;
        }
        return false;
    }

    @Override
    public String toString() {
        return "[" + x + "," + y + ", " + z + "]";
    }

}

public class Practice2 {
    public static void main(String[] args) {
        Point3D p1 = new Point3D(1, 2, 3);
        Point3D p2 = new Point3D(1, 2, 3);

        System.out.println(p1);
        System.out.println(p2);

        System.out.println("p1 == p2?" + (p1 == p2));
        System.out.println("p1.equals(p2)?" + p1.equals(p2));
    }
}

  1. 다음과 같은 실행 결과가 나오도록 코딩하시오.

String fullPath = "c:\jdk1.8\work\PathSeparateTest.java";

// 실행 결과
fullPath:c:\jdk1.8\work\PathSeparateTest.java
path:c:\jdk1.8\work
fileName:PathSeparateTest.java

package com.test.memo;

public class Practice2 {
    public static void main(String[] args) {
        String fullPath = "c:\\jdk1.8\\work\\PathSeparateTest.java";

        String path = null;
        String fileName = null;

        int pos = fullPath.lastIndexOf("\\");
        if (pos != -1) {
            path = fullPath.substring(0, pos);
            fileName = fullPath.substring(pos + 1);
        }

        System.out.println(fullPath);
        System.out.println(path);
        System.out.println(fileName);

    }
}

  1. 다음과 같은 실행결과가 나오도록 프로그램을 작성하자.

class Exercise9_5 {
public static void main(String[] args) {
System.out.println(count("12345AB12AB345AB","AB"));
System.out.println(count("12345","AB"));
}
}

[실행결과]
3
0

package com.test.memo;

public class Practice2 {

    private static int count(String str, String ch) {

        int count = 0;
        int pos = 0; // 찾기 시작할 위치

        while ((pos = str.indexOf(ch, pos)) != -1) {
            count++;
            pos += ch.length();
        }
        return count;
    }

    public static void main(String[] args) {
        System.out.println(count("12345AB12AB345AB", "AB"));
        System.out.println(count("12345", "AB"));
    }
}

  1. 다음 fillZero 메소드를 코딩하시오.

(1) fillZero 메서드를 작성하시오.

  1. src가 널이거나 src.length()가 length와 같으면 src를 그대로 반환한다.
  2. length의 값이 0보다 같거나 작으면 빈 문자열("")을 반환한다.
  3. src의 길이가 length의 값보다 크면 src를 length만큼 잘라서 반환한다.
  4. 길이가 length인 char배열을 생성한다.
  5. 4에서 생성한 char배열을 '0'으로 채운다.
  6. src에서 문자배열을 뽑아내서 4에서 생성한 배열에 복사한다.
  7. 4에서 생성한 배열로 String을 생성해서 반환한다.

import java.util.Arrays;
class Exercise9_6 {
public static String fillZero(String src, int length) {
}
public static void main(String[] args) {
String src = "12345";
System.out.println(fillZero(src,10));
System.out.println(fillZero(src,-1));
System.out.println(fillZero(src,3));
}
}

// 실행 결과
0000012345
123

package com.test.memo;

import java.util.Arrays;

public class Practice2 {

    public static String fillZero(String src, int length) {
        if(src == null || src.length() == length) {
            return src;
        }
        if(length <= 0) {
            return " ";
        }
        if(src.length() > length) {
            return src.substring(0,length);
        }

        char[] ch = new char[length];
        Arrays.fill(ch, '0');

        char[] srcCh = src.toCharArray(); //문자열을 char형 배열로 변환시키는 

        System.arraycopy(srcCh, 0, ch, length - src.length(), src.length());

        return new String(ch);
    }
    public static void main(String[] args) {
        String src = "12345";
        System.out.println(fillZero(src,10));
        System.out.println(fillZero(src,-1));
        System.out.println(fillZero(src,3));

    }
}
  1. String 을 배열로 변환시키지 않고, 배역 복사를 시도해서 예외가 발생했다. toCharArray()를 이용해서 char형 배열로 변경시켰다.

  2. 만약 src가 length보다 클때 substring을 이용해서 잘라서 반환하는데, (0, length) 가 아닌 (length)로 둬서 length값 부터 끝까지로 되어서 45가 반환되었다.


  1. 다음과 같은 실행결과가 나오도록 프로그램을 작성하자.

class Exercise9_7 {

    public static void main(String[] args) {
System.out.println(contains("12345","23"));
System.out.println(contains("12345","67"));
}

}

[실행결과]
true
false

package com.test.memo;

public class Practice2 {

    private static boolean contains(String str, String ch) {
        if (str.contains(ch)) {
            return true;
        }
        return false;

    }

    public static void main(String[] args) {

        System.out.println(contains("12345", "23"));
        System.out.println(contains("12345", "67"));

    }
}

  1. 다음과 같은 실행결과가 나오도록 프로그램을 작성하자.

class Exercise9_8 {
public static void main(String[] args) {
System.out.println(round(3.1415,1));
System.out.println(round(3.1415,2));
System.out.println(round(3.1415,3));
System.out.println(round(3.1415,4));
System.out.println(round(3.1415,5));
}
}

[실행결과]
3.1
3.14
3.142
3.1415
3.1415

package com.test.memo;

public class Practice2 {

    private static double round(double d, int n) {

        double num = Math.pow(10, n);

        double result = Math.round(d * num);
        return result / num;

    }

    public static void main(String[] args) {

        System.out.println(round(3.1415, 1));
        System.out.println(round(3.1415, 2));
        System.out.println(round(3.1415, 3));
        System.out.println(round(3.1415, 4));
        System.out.println(round(3.1415, 5));

    }
}

원리

public static double round(double d, int n) {
    double power = Math.pow(10, n); // 10의 n승 계산
    return Math.round(d * power) / power; // 반올림 후 다시 원래 자리로 돌아가기
}
  • 우선 Math.pow() 메서드를 사용하여 10의 n승을 계산하여 소수점 이하 n자리를 구한다.

  • 그런 다음 Math.round() 메서드를 사용하여 소수점 이하 n자리까지 반올림된 값을 구하고, 마지막으로 다시 10의 -n승으로 나누어 원래 자리로 돌아갑니다.


  1. 다음과 같은 실행결과가 나오도록 프로그램을 작성하자.

class Exercise9_9 {

    public static void main(String[] args) {
System.out.println("(1!2@3^4~5)"+" -> "
+ delChar("(1!2@3^4~5)","~!@#$%^&*()"));
System.out.println("(1 2 3 4 5)"+" -> "
+ delChar("(1 2 3 4 5)"," \t"));
}

}

실행결과 -> 12345
(1 2 3 4 5) -> (12345)

package com.test.memo;

public class Practice2 {

    private static String delChar(String str, String pr) {

        StringBuffer sb = new StringBuffer();

        for (int i = 0; i < str.length(); i++) {
            char ch = str.charAt(i);
            if (pr.indexOf(ch) == -1) {
                sb.append(ch);
            }
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        System.out.println("(1!2@3^4~5)" + " -> " + delChar("(1!2@3^4~5)", "~!@#$%^&*()"));
        System.out.println("(1 2 3 4 5)" + " -> " + delChar("(1 2 3 4 5)", " \t"));

    }
}

  1. 다음과 같은 실행결과가 나오도록 프로그램을 작성하자.

class Exercise9_12
{

    public static void main(String[] args)
{
for(int i=0; i< 20; i++)
System.out.print(getRand(1,-3)+",");
}

}

// 실행 결과(실행 결과는 다를 수 있다.)
-3,-2,-2,-2,0,1,-1,-2,0,-3,-1,-1,-2,-1,-1,1,-3,-3,-3,-1,

package com.test.memo;

public class Practice2 {

    private static int getRand(int num1, int num2) {

        int max = Math.max(num1, num2);
        int min = Math.min(num1, num2);

        if (max == min) {
            return max;
        }

        int ran = (int) (Math.random() * (Math.abs(max - min + 1)) + min);
        return ran;

    }

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++)
            System.out.print(getRand(1, -3) + ",");
    }
}

굳이 Math.abs()를 사용하지 않아도 같다.


  1. 다음과 같이 실행결과가 나오도록 코딩하시오.

public class Exercise9_13 {
public static void main(String[] args) {
String src = "aabbccAABBCCaa";
System.out.println(src);
System.out.println("aa를" + stringCount(src, "aa") +"개 찾았습니다.");
}
static int stringCount(String src, String key) {
}
static int stringCount(String src, String key, int pos) {
}
}

// 실행 결과
aabbccAABBCCaa
aa를2개 찾았습니다.

package com.test.memo;

class Practice2 {
    public static void main(String[] args) {
        String src = "aabbccAABBCCaa";
        System.out.println(src);
        System.out.println("aa를" + stringCount(src, "aa") + "개 찾았습니다.");
    }

    static int stringCount(String src, String key) {
        return stringCount(src, key, 0);
    }

    static int stringCount(String src, String key, int pos) {
        int count = 0;
        int index = 0;
        if (key == null || key.length() == 0)
            return 0;

        while ((index = src.indexOf(key, pos)) != -1) {
            count++;
            pos = index + key.length();
        }

        return count;
    }
}
  1. while문을 돌리지 않아서 1개밖에 찾지 못했다.

  2. 만약 int가 없다면 오버로딩한 다른 메소드로 리턴시키면 됐는데 그 생각을 못했다.


  1. 다음과 같이 실행결과가 나오도록 코딩하시오.

class Exercise9_14 {
public static void main(String[] args) {
String[] phoneNumArr = {
"012-3456-7890",
"099-2456-7980",
"088-2346-9870",
"013-3456-7890"
};
}
}

// 실행 결과

입력: 234
[012-3456-7890, 088-2346-9870]

입력:

package com.test.memo;

import java.util.Scanner;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Practice2 {
    public static void main(String[] args) {
        String[] phoneNumArr = { "012-3456-7890", "099-2456-7980", "088-2346-9870", "013-3456-7890" };
        String finNum = null;
        Vector list = new Vector<>();

        Scanner sc = new Scanner(System.in);

        while (true) {
            System.out.print("입력: ");
            finNum = sc.next().trim();
            if (finNum.equals(" ")) {
                System.out.println("찾을 전화번호를 입력하세요.");
                continue;
            } else if (finNum.equalsIgnoreCase("Q")) {
                System.out.println("시스템을 종료합니다.");
                System.exit(0);
            }

            String pattern = ".*" + finNum + ".*";
            Pattern p = Pattern.compile(pattern);

            for (int i = 0; i < phoneNumArr.length; i++) {
                String phoneNum = phoneNumArr[i];
                String tmp = phoneNum.replace("-", "");

                Matcher m = p.matcher(tmp);

                if (m.find()) {
                    list.add(phoneNum);// 패턴과 일치하면 list에 번호 추가
                }
            }
            if (list.size() > 0) {// 검색 결과가 존재한다면,
                System.out.println(list);// 출력하고
                list.clear();// 삭제
            } else
                System.out.println("일치하는 번호가 없습니다.");
        }
    }
}
  • 벡터와 패턴, 매쳐를 사용해서 코딩 >> 나중에 한번 더 연습해보기

배열 문제

  1. 주어진 예제 ArrayObjSort.java에서는 Person의 인스턴스들을 나이순으로 정렬하였는데, 이를 수정하여 나이의 역순으로 정렬이 되도록 해보자. 다시 말해서, 많은 나이의 인스턴스일수록 배열의 앞쪽에 위치하도록 예제를 수정해보자.
package com.test.memo;

import java.util.Arrays;

class Person implements Comparable<Person> {
    private String name;
    private int age;

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

    @Override
    public String toString() {
        return name + ": " + age;
    }

    @Override
    public int compareTo(Person p) {
        return p.age - this.age;
    }

}

public class Practice2 {
    public static void main(String[] args) {
        Person[] ar = new Person[3];

        ar[0] = new Person("Lee", 29);
        ar[1] = new Person("Goo", 15);
        ar[2] = new Person("Soo", 37);

        Arrays.sort(ar);

        for (Person p : ar)
            System.out.println(p);

    }
}
  • 먼저 Comparble을 구현하고, 단축키로 오버라이딩해줘야 뜬다.

  1. 주어진 예제 ArrayObjSort.java에서는 Person의 인스턴스들을 나이순으로 정렬하였는데, 이를 이름의 길이 순으로 정렬이 되도록 수정해보자. 즉 이름이 길이가 짧은 인스턴스일수록 배열의 앞쪽에 위치하도록 예제를 수정해야 한다.
@Override
    public int compareTo(Person p) {
        return this.name.length() - p.name.length();
    }
  • 이름의 길이를 이용해서 빼주면 길이가 짧은 순으로 출력되고, 반대로 빼주면 이름이 긴순으로 출력된다.

Arrays클래스 - binarySearch

Arrays클래스는 모든 메서드가 static(정적) 메서드다. > 객체 생성 x 하고도, 사용 가능

  • public static int binarySearch(int[] a, int key)

    • 배열 a에서 key를 찾아서 해당 수가 있으면 key의 인덱스 값, 없으면 -1 반환

예제

import java.util.Arrays;

class ArraySearch {
    public static void main(String[] args) {
        int[] ar = {33, 55, 11, 44, 22};

        Arrays.sort(ar);    // 탐색 이전에 정렬이 선행되어야 한다.
        for(int n : ar)
            System.out.print(n + "\t");
        System.out.println();

        int idx = Arrays.binarySearch(ar, 33);
        System.out.println("Index of 33: " + idx);

    }
}

/*
11    22    33    44    55    
Index of 33: 2
*/

배열 검색은 정렬된 상태의 데이터를 대상으로 하는 탐색 알고리즘이다.

위에서 보이듯, 배열의 정렬을 한 이후에 binarySearch메소드를 호출해야 한다.

  • binarySearch메소드는 Object형 배열에 대해서도 오버로딩 되어있다.

  • public static int binarySearch(Object[] a, Object key)

    • 메소드가 key와 동일한 인스턴스를 찾았다고 판단하는 기준을 Comparable 인터페이스의 compareTo메소드에서 세워줘야 한다.

      • compareTo메소드를 오버라이딩해서 호출을 통해 0이 반환되면 key에 해당하는 인스턴스를 찾았다고 판단한다.
import java.util.Arrays;

class Person implements Comparable {
    private String name;
    private int age;

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

    @Override
    public int compareTo(Object o) {
        Person p = (Person)o;
        return this.age - p.age; //나이가 같으면 0을반 
    }

    @Override
    public String toString() {
        return name + ": " + age;
    }
}


class ArrayObjSearch {
    public static void main(String[] args) {
        Person[] ar = new Person[3];

        ar[0] = new Person("Lee", 29);
        ar[1] = new Person("Goo", 15);
        ar[2] = new Person("Soo", 37);

        Arrays.sort(ar);

        int idx = Arrays.binarySearch(ar, new Person("Who are you?", 37));
        System.out.println(ar[idx]);
    }
}

/*
실행결과
Soo: 37
*/
  • 현재 코드에서 기준이 되는 compareTo()메서드에서 age를 가지고 나이를 비교하고있기 때문에 이름은 중요하지 않고, 나이를 찾고있다.

    • 문장의 내용은 "Who are you?" 나이가 37인 Person 인스턴스를 찾는 것이지만, 탐색의 결과는 이름이 "Soo"이고 나이가 37이 출력된 것을 알 수 있다.

binarySearch 메소드를 통해 인스턴스를 찾고자 하는 경우, 탐색의 대상이 되는 인스턴스들의 클래스는 Comparable 인터페이스를 구현한 상태이어야 한다. 이는 compareTo 메소드의 구현 내용을 토대로 탐색이 진행되기 때문이다.


탐색의 기준 문제

import java.util.Arrays;

class Person implements Comparable {
    private String name;
    private int age;

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

    @Override
    public int compareTo(Object o) {
        Person p = (Person)o;
        return this.age - p.age;
    }

    @Override
    public String toString() {
        return name + ": " + age;
    }
}


class ArrayObjSearch {
    public static void main(String[] args) {
        Person[] ar = new Person[3];

        ar[0] = new Person("Lee", 29);
        ar[1] = new Person("Goo", 15);
        ar[2] = new Person("Soo", 37);

        Arrays.sort(ar);

        int idx = Arrays.binarySearch(ar, new Person("Who are you?", 37));
        System.out.println(ar[idx]);
    }
}

탐색의 기준 변경
앞서 예제 ArrayObjSearch.java에서 탐색의 기준은 나이였다. 그런데 이 탐색의 기준이 이름이 되도록 예제를 수정해보자.

정답

import java.util.Arrays;

class Person implements Comparable {
    private String name;
    private int age;

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

    @Override
    public int compareTo(Object o) {
        Person p = (Person)o;
        return this.name.compareTo(p.name); //이름을 기준으로 오름차순 
    }

    @Override
    public String toString() {
        return name + ": " + age;
    }
}


class ArrayObjSearch {
    public static void main(String[] args) {
        Person[] ar = new Person[3];

        ar[0] = new Person("Lee", 29);
        ar[1] = new Person("Goo", 15);
        ar[2] = new Person("Soo", 37);

        Arrays.sort(ar);

        int idx = Arrays.binarySearch(ar, new Person("Goo", 15));
        System.out.println(ar[idx]);
    }
}
  • 나 풀때는 이름의 사전적 비교가 아닌, 이름의 길이로 생각했다.. 문제를 제대로 안본듯

  • 만약 제네릭 형식이라면 아래와 같이 변경된다.

  class Person implements Comparable<Person>{ 
      private String name;
      private int age;

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

      @Override
      public int compareTo(Person p) {
          return this.name.compareTo(p.name);
      }

      @Override
      public String toString() {
          return name + ": " + age;
      }

  }

toString()

Object클래스를 참조하기 때문에 참조하는 값이 출력되는 것이다.

import java.util.Arrays;
import java.util.Date;

class Test {
  public static void main(String[] args) {
      int[] arr = {1, 2, 3};
      Date date = new Date();
      System.out.println(arr);            // [I@279f2327
      System.out.println(date);            // Mon Mar 18 11:12:19 KST 2024
      System.out.println(arr.toString());        // [I@279f2327
      System.out.println(date.toString());        // Mon Mar 18 11:12:19 KST 2024
      System.out.println(Arrays.toString(arr));        // [1, 2, 3]
  }
}
  • 배열의 값을 출력하고 싶을때 Arrays.toString()을 통해 값을 출력할 수 있다.

java.math

정수 자료형의 문제점인 표현범위의 한계를 보강하기 위한 BigInteger

BigInteger클래스는 문자열 형태로 이루어져 있어 숫자의 범위가 무한하다.

실수 자료형의 문제점인 오차를 보강하기 위한 BigDecimal


BigInteger

BigInteger() - 매개변수는 String

선언 방법(BigDecimal과 동일)

BigInteger bigNumber = new BigInteger("10000");
  • BigInteger을 초기화하기 위해서는 문자열을 인자 값으로 넘겨주어야 한다.

    BigInteger가 문자열로 되어있기 때문이다.


계산 메서드(BigDecimal과 동일)

BigInteger은 문자열이기 때문에 사칙연산이 안된다.

BigInteger 클래스 내부에 있는 메서드를 사용해야 한다.

BigInteger bigNumber1 = new BigInteger("100000");
BigInteger bigNumber2 = new BigInteger("10000");

System.out.println("덧셈(+) :" +bigNumber1.add(bigNumber2));
System.out.println("뺄셈(-) :" +bigNumber1.subtract(bigNumber2));
System.out.println("곱셈(*) :" +bigNumber1.multiply(bigNumber2));
System.out.println("나눗셈(/) :" +bigNumber1.divide(bigNumber2));
System.out.println("나머지(%) :" +bigNumber1.remainder(bigNumber2));


형변환 방법(BigDecimal과 동일)

BigInteger bigNumber = BigInteger.valueOf(100000); //int -> BigIntger

int int_bigNum = bigNumber.intValue(); //BigIntger -> int
long long_bigNum = bigNumber.longValue(); //BigIntger -> long
float float_bigNum = bigNumber.floatValue(); //BigIntger -> float
double double_bigNum = bigNumber.doubleValue(); //BigIntger -> double
String String_bigNum = bigNumber.toString(); //BigIntger -> String

값 비교(BigDecimal과 동일)

값을 비교할 때는 compareTo라는 메서드를 사용한다.

BigInteger bigNumber1 = new BigInteger("100000");
BigInteger bigNumber2 = new BigInteger("1000000");

//두 수 비교 compareTo 맞으면 0   틀리면 -1
int compare = bigNumber1.compareTo(bigNumber2);
System.out.println(compare);

예제

import java.math.*;

class SoBigInteger
{
    public static void main(String[] args)
    {
        System.out.println("최대 정수: " + Long.MAX_VALUE);
        System.out.println("최소 정수: " + Long.MIN_VALUE);

        BigInteger bigValue1=new BigInteger("100000000000000000000");
        BigInteger bigValue2=new BigInteger("-99999999999999999999");

        BigInteger addResult=bigValue1.add(bigValue2);
        BigInteger mulResult=bigValue1.multiply(bigValue2);

        System.out.println("큰 수의 덧셈결과: "+addResult);
        System.out.println("큰 수의 곱셈결과: "+mulResult);
    }
}
  • bigValue1의 값을 가질 수 있는 자료형이 없기 때문에 문자열 형태로 객체를 생성한다.

    • add는 덧셈

    • multiply는 곱셈

BigDecimal() - 문자열이기 때문에 연산이 불가능하고, compareTo를 이용해 값을 비교해야한다.


BigDecimal

class DoubleError
{
    public static void main(String[] args)
    {
        double e1=1.6;
        double e2=0.1;

        System.out.println("두 실수의 덧셈결과: " + (e1+e2));
        System.out.println("두 실수의 곱셈결과: " + (e1*e2));
    }
}
  • 위 코드를 출력해보면 오차가 발생한다.

    • 실수는 오차가 있기때문에 BigDecimal은 실수의 오차를 없이 출력할 수 있도록 존재하는 클래스다.

      BigDecimal e1=new BigDecimal("1.6");
      BigDecimal e2=new BigDecimal("0.1");

      • 그러나 이렇게 연산해도 오차가 발생하는 것을 볼 수 있다.
      • 그 이유는 사칙연산이 안되기 때문에 클래스내 메서드를 이용해서 계산해줘야 한다.

선언 방법

BigInteger와 같이 인자 값을 문자열로 넘겨줘야 한다.

BigDecimal bigNumber = new BigDecimal("10000.12345");

계산 메서드

BigDecimal bigNumber1 = new BigDecimal("100000.12345");
BigDecimal bigNumber2 = new BigDecimal("10000");

System.out.println("덧셈(+) :" +bigNumber1.add(bigNumber2));
System.out.println("뺄셈(-) :" +bigNumber1.subtract(bigNumber2));
System.out.println("곱셈(*) :" +bigNumber1.multiply(bigNumber2));
System.out.println("나눗셈(/) :" +bigNumber1.divide(bigNumber2));
System.out.println("나머지(%) :" +bigNumber1.remainder(bigNumber2));
  • BigInteger와 같다.

BigDecimal 예제

프로그램 사용자로부터 두 개의 실수를 입력 받은 후, 두 실수의 차에 대한 절대값을 계산하여 출력하는 프로그램을 작성하자.

정답

import java.util.Scanner;
import java.math.BigDecimal;

class BigDecimalABS
{
    public static void main(String[] args)
    {
        Scanner sc = new Scanner(System.in);
        System.out.print("실수 1 입력 : ");
        String val1 = sc.nextLine();

        System.out.println("실수 2 입력 : ");
        String val2 = sc.nextLine();

        BigDecimal e1 = new BigDecimal(val1);
        BigDecimal e2 = new BigDecimal(val2);

        BigDecimal subResult = e1.subtract(e2);
        System.out.println("두 실수의 차에 대한 절대값 : " + subResult.abs());
    }
}
  • .substract() - BigDecimal 객체간의 뺼셈을 수행한다.

    • 객체를 뺀 결과를 반환한다.

Math 클래스

수학 관련 연산

Random

import java.util.Random;

class PseudoRandom
{    
    public static void main(String[] args)
    {
        Random rand=new Random(12); //seed : '시드' 를 가지고 난수 생

        for(int i=0; i<100; i++)
            System.out.println(rand.nextInt(1000));
    }
}
  • 시드를 가지고 난수를 생성하는 예제이다. > 시드가 같으면 몇번을 컴파일해도 같은 난수가 발생한다.

    • 컴퓨터에서 생성하는 난수의 근거, 재료가 되는 하나의 숫자를 기반으로 만들어지도록 알고리즘이 설계되어 있다.

System.currentTimeMillis()

import java.util.Random;

class SeedChangeRandom
{    
    public static void main(String[] args)
    {
        Random rand=new Random(12);
        rand.setSeed(System.currentTimeMillis());

        for(int i=0; i<100; i++)
            System.out.println(rand.nextInt(1000));
    }
}
  • etSeed(System.currentTimeMillis() - 난수 발생기의 시드값을 변경시켜주는 효과가 있다. (12)부분에 넣어도 상관없다.

    • 정확히는 현재 시스템 시간(1970년 1월 1일 00:00:00 UTC부터 현재까지 경과한 밀리초)을 반환하는 것이기 때문에 시드값이 변경되는 원리인것이다.

StringTokenizer 클래스 - 문자열 분리

문자열을 구분자를 이용하여 분리할 때 사용할 수 있다.

문자열을 토큰화 시킨다. 라고 생각하면 된다.

토큰은 분리된 문자열 조각으로, StringTokenizer클래스는 하나의 문자열을 여러 개의 토큰으로 분리하는 클래스다.

StringTokenizer 와 split 의 차이

둘 다 문자열을 파싱하는데 사용할 수 있다.

  • StringTokenizer는 java.util에 포함되어 있는 클래스 / split는 String 클래스에 속해있는 메소드이다.

  • StringTokenizer는 문자 또는 문자열로 문자열을 구분 / split는 정규표현식으로 구분

  • StringTokenizer는 빈 문자열을 토큰으로 인식 X / split는 빈 문자열을 토큰으로 인식

  • StringTokenizer는 결과값이 문자열 / split는 결과값이 문자열 배열

    따라서 StringTokenizer를 이용할 경우 전체 토큰을 보고싶다면 반복문을 이용해 하나하나 뽑아야 한다.

생성자 선언 방식

//1. 띄어쓰기 기준으로 문자열을 분리 > 구분자를 넣지 않으면, 자동으로 공백으로 분리한다.
StringTokenizer st = new StringTokenizer(문자열);

//2. 구분자를 기준으로 문자열을 분리
StringTokenizer st = new StringTokenizer(문자열, 구분자);

/* + 구분자를 기준으로 문자열을 분리할 때 구분자도 토큰으로 넣는다.(true)
 * 구분자를 분리된 문자열 토큰에 포함 시키지 않는다.(false) > 디폴트로 false로 되어있다.
 */
 StringTokenizer st = new StringTokenizer(문자열, 구분자, true/false);

메서드

리턴값메서드명역할
booleanhasMoreTokens()남아있는 토큰이 있으면 true를 리턴, 더 이상 토큰이 없으면 false 리턴
StringnextToken()객체에서 다음 토큰을 반환
StringnextToken(String str)str 기준으로 다음 토큰은 반환
booleanhasMoreElements()hasMoreTokens와 동일 <> 토큰으로 된 메서드를 주로 사용
ObjectnextElement()nextToken메서드와 동일하지만 문자열이 아닌 객체를 리턴
intcountTokens()총 토큰의 개수를 리턴

StringTokenizer() - 문자열을 파싱할 때 쓰는 메소드

import java.util.StringTokenizer;

class TokenizeString
{    
    public static void main(String[] args)
    {
        String strData="11:22:33:44:55";
        StringTokenizer st=new StringTokenizer(strData, ":"); //분해할 문자열 지정, 구분

        while(st.hasMoreTokens()) //Token이 남았다면 true, 없다면 false 
            System.out.println(st.nextToken());
    }
}//출력 : 11 22 33 44 55 

현재 구분자를 콜론을 주었으니까, 콜론을 기준으로 분해를 하는 것이다.

콜론을 기준으로 커서가 처음부터 옮겨서 분해된다.

  • .hasMoreTokens() - 구분자를 기준으로 분해 할 token이 남았는지 안남았는지를 boolean으로 반환하는 메서드

    • 원하는 데이터를 parsing(파싱) 해서 가져올때 토큰을 이용해 데이터를 쪼개서 가져온다는 등의 활용이 가능하다.

예제 2

import java.util.StringTokenizer;

class TokenizeString2
{    
    public static void main(String[] args)
    {
        String phoneNum="TEL 82-02-997-2059";     //국제 전화번호
        String javaCode="num+=1";

        System.out.println("First Result...........");
        StringTokenizer st1=new StringTokenizer(phoneNum); 
        //위와 달리 구분자를 주지 않았다면, 공백을 기준으로 잘라낸다. > 공백이 구분자가 되는것 

        while(st1.hasMoreTokens())
            System.out.println(st1.nextToken()); //출력 Tel 82-02-997-2059 

        System.out.println("\nSecond Result...........");
        StringTokenizer st2=new StringTokenizer(phoneNum, " -");
        //구분자를 공백과 - 를 준 것 > 공백과 대쉬로 구분자가 존재하는것이다. 
        while(st2.hasMoreTokens())
            System.out.println(st2.nextToken()); //출력 Tel 82 02 997 2059 

        System.out.println("\nThird Result...........");
        StringTokenizer st3=new StringTokenizer(javaCode, "+=", true);
         //구분자가 + 와 =이고, true는 구분자 또한 토큰으로 구분한다는 것이다. 
        while(st3.hasMoreTokens())
            System.out.println(st3.nextToken());//출력 num + = 1 
    }
}
  • 구분자를 주지 않았다면 공백을 기준으로 나눈다.
  • 구분자를 " -" 로 주었기 때문에, 공백과 -(대쉬) 로 구분자를 사용하는것이다.
  • 구분자에 true가 존재하면 구분자 또한 토큰으로 구분한다.

구분자는 하나의 문자로만 구성될 필요 없고, 여러 개의 문자를 구분자로 토큰을 만들 수 있다.


문법 연습 문제

  1. short 그리고 int와 같은 정수 자료형의 문제점은 매우 큰 수의 표현이 불가능하다는데 있고, float그리고 double과 같은 실수 자료형의 문제점은 정밀한 값의 표현이 불가능해 항상 오차가 존재한다는데 있다. 자바는 이러한 문제점의 해결을 위해서 ( ) 클래스와 ( ) 클래스를 제공하고 있다.정답
  • BigInteger / BigDecimal

  1. Wrapper 클래스의 static 변수를 이용하여 가장 큰 수와 가장 작은 수를 출력하시오.정답

내가 푼 문제

package com.test.memo;

public class Practice2 {
    public static void main(String[] args) {

        System.out.println(Integer.MAX_VALUE);
        System.out.println(Integer.MIN_VALUE);

    }
}

  1. 1번에서 설명한 클래스를 이용하여 100000000000000000000 와 -99999999999999999999 를 각각 저장하고 두 수를 서로 더하고 곱한 결과를 출력하시오.정답

내가 푼 문제

package com.test.memo;

import java.math.BigInteger;

public class Practice2 {
    public static void main(String[] args) {

        BigInteger b1 = new BigInteger("100000000000000000000");
        BigInteger b2 = new BigInteger("-99999999999999999999");

        System.out.println(b1.add(b2));
        System.out.println(b1.multiply(b2));

    }
}

선생님 풀이

import java.math.*;

class SoBigInteger
{
    public static void main(String[] args)
    {
        System.out.println("최대 정수: " + Long.MAX_VALUE);
        System.out.println("최소 정수: " + Long.MIN_VALUE);

        BigInteger bigValue1=new BigInteger("100000000000000000000");
        BigInteger bigValue2=new BigInteger("-99999999999999999999");

        BigInteger addResult=bigValue1.add(bigValue2);
        BigInteger mulResult=bigValue1.multiply(bigValue2);

        System.out.println("큰 수의 덧셈결과: "+addResult);
        System.out.println("큰 수의 곱셈결과: "+mulResult);
    }
}

  1. 1번에서 말한 클래스를 이용하여 실수 1.6과 0.1을 더한 값과 곱한 값을 각각 구하시오.정답

내가 푼 문제

package com.test.memo;

import java.math.BigDecimal;

public class Practice2 {
    public static void main(String[] args) {

    BigDecimal b1 = new BigDecimal(1.6);
    BigDecimal b2 = new BigDecimal(0.1);

    System.out.println(b1.add(b2));
    System.out.println(b1.multiply(b2));

    }
}

  1. 컴퓨터의 현재시간을 기준으로, 1970년 1월1일 자정 이후로 지나온 시간을 밀리 초(1/1000초) 단위로 계산해서 반환하는 메소드는?
  • System.currentTimeMillis()

  1. 매개변수 없는 난수 발생의 생성자의 원리는?
public Random()
{
 this(System.currentTimeMillis()); // 씨드 값을 전달받는 또 다른 생성자의 호출
}
  • API 문서에서는 "Random의 생성자가 호출될 때, 이전에 호출될 때와 전혀 다른 씨드 값이 설정된다.

    • Math 클래스에 static으로 선언되어 있는 random 메소드를 이용해서 난수를 생성하는 방법도 존재한다.

    • Math 클래스에 정의되어 있는 random 메소드는 Random 클래스의 nextDouble 메소드와 마찬가지로 0.0 이상 1.0 미만의 double 형 난수를 반환한다.

      • 물론 실행할 때마다 새로운 씨드 값이 설정되기 때문에, 실행할 때마다 다른 유형의 난수가 생성된다.

  1. 다음 토큰 반환할 때 쓰는 메소드 정답
  • public String nextToken()

  1. 반환할 토큰이 남았는 지 확인하는 메소드 정답
  • public boolean hasMoreTokens()

  1. "11:22:33:44:55" 를 ":"를 구분자로 하여 각각의 토큰을 출력하시오.정답

내가 푼 풀이

package com.test.memo;

import java.util.StringTokenizer;

public class Practice2 {
    public static void main(String[] args) {

        String time = "11:22:33:44:55";

        StringTokenizer str1 = new StringTokenizer(time, ":");
        while (str1.hasMoreTokens())
            System.out.print(str1.nextToken() + " ");
    }
}

  1. 아래와 같이 출력하게 하시오. 정답 근데 좀 아쉽

"TEL 82-02-997-2059"를 " "을 구분자로 하여 각각의 토큰을 출력하시오.
"TEL 82-02-997-2059"를 " -"을 구분자로 하여 각각의 토큰을 출력하시오.
"num+=1"를 "+="을 구분자로 하여 각각의 토큰을 출력하시오. 구분자도 토큰으로 인식하도록하여 출력하자.

내가 푼 풀이

package com.test.memo;

import java.util.StringTokenizer;

public class Practice2 {
    public static void main(String[] args) {

        String Tel1 = "TEL 82-02-997-2059";
        String Tel2 = "TEL 82-02-997-2059";
        String num = "num+=1";

        StringTokenizer str1 = new StringTokenizer(Tel1, " ");
        while (str1.hasMoreTokens())
            System.out.print(str1.nextToken() + " ");

        System.out.println();

        StringTokenizer str2 = new StringTokenizer(Tel2, " -");
        while (str2.hasMoreTokens()) {
            System.out.print(str2.nextToken() + " ");
        }

        System.out.println();

        StringTokenizer str3 = new StringTokenizer(num, "+=", true);
        while (str3.hasMoreTokens()) {
            System.out.print(str3.nextToken() + " ");
        }
    }
}
  • str1에서 구분자를 주지 않으면, 자동으로 공백을 구분자로 준다.

    • StringTokenizer st1=new StringTokenizer(phoneNum); > 이렇게 주면 됐던것

Generic

데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법

  1. 여러 타입을 지원하는 클래스를 추상화 할 수 있다.

    • 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것이다.

      • 특정 타입을 미리 지정해주는 것이 아닌 필요에 의해 지정할 수 있도록 하는 일반(Generic)타입

Generic의 장점

  1. 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.
  2. 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다. 즉, 관리하기가 편하다.
  3. 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.

Generic 사용 방법

의미를 두어 이름을 짓는다. > 한 문자로 짓거나, 대문자로 이름을 짓는다.

타입설명
\Type
\Element
\Key
\Value
\Number

암묵적인 규칙으로 위와 같이 선언한다.

1. 클래스 및 인터페이스 선언

public class ClassName <T> { ... }
public interface InterfaceName <T> { ... }

T타입은 해당 블럭{...} 안에서까지 유효하다.

  • 여기서 더 나아가 제네릭 타입을 두 개로 둘 수도있다.(아래와 같이)

2. 제네릭 클래스

public class ClassName <T, K> { ... }
public interface InterfaceName <T, K> { ... }

// HashMap의 경우 아래와 같이 선언되어있을 것이다.
public class HashMap <K, V> { ... }

위와 같이 데이터 타입을 외부로부터 지정할 수 있도록 할 수 있다.

  • 이렇게 생성되 제네릭 클래스를 메인 메소드에서 아래와 같이 사용한다.

제네릭 클래스 사용 방법

public class ClassName <T, K> { ... }

public class Main {
    public static void main(String[] args) {
        ClassName<String, Integer> a = new ClassName<String, Integer>();
    }
}

객체를 생성해야 하는데, 이 때 구체적인 타입을 명시해야한다.

위 에시대로라면 T는 String이 되고, K는 Integer가 된다.

  • new생성자로 클래스 객체를 생성하고, <> 괄호 사이에 파라미터로 넘겨준 타입으로 지정된다.

  • 주의해야 할점

    • 파라미터로 명시할 수 있는 것은 참조 타입(Reference Type) 밖에 올 수 없다.

      • int, double, char >> primitive type은 명시 불가능 >> Wrapper Class가능

      • 그래서 int형double형 등 primitive Type의 경우 Integer, Double같은 Wrapper Type으로 쓰는 이유다.

    • 참조 타입이 올 수 있다는 것은 사용자가 정의한 클래스도 타입으로 올 수 있다

      public class ClassName <T> { ... }
      
      public class Student { ... }
      
      public class Main {
          public static void main(String[] args) {
              ClassName<Student> a = new ClassName<Student>();
          }
      }

예제

예제 - 제네릭 한 개

// 제네릭 클래스
class ClassName<E> {

    private E element;    // 제네릭 타입 변수

    void set(E element) {    // 제네릭 파라미터 메소드
        this.element = element;
    }

    E get() {    // 제네릭 타입 반환 메소드
        return element;
    }

}

class Main {
    public static void main(String[] args) {

        ClassName<String> a = new ClassName<String>();
        ClassName<Integer> b = new ClassName<Integer>();

        a.set("10");
        b.set(10);

        System.out.println("a data : " + a.get());
        // 반환된 변수의 타입 출력 
        System.out.println("a E Type : " + a.get().getClass().getName());

        System.out.println();
        System.out.println("b data : " + b.get());
        // 반환된 변수의 타입 출력 
        System.out.println("b E Type : " + b.get().getClass().getName());

    }
}
  • ClassName이라 객체를 생성할 때 <> 안에 파라미터를 지정한다.

    • 그러면 a객체의 E제네릭 타입을 String으로 모두 변환되고,

    • b객체의 ClassName의 E제네릭 타입은 Integer로 모두 변환된다.

예제 제네릭 두 개

// 제네릭 클래스 
class ClassName<K, V> {    
    private K first;    // K 타입(제네릭)
    private V second;    // V 타입(제네릭) 

    void set(K first, V second) {
        this.first = first;
        this.second = second;
    }

    K getFirst() {
        return first;
    }

    V getSecond() {
        return second;
    }
}

// 메인 클래스 
class Main {
    public static void main(String[] args) {

        ClassName<String, Integer> a = new ClassName<String, Integer>();

        a.set("10", 10);


        System.out.println("  fisrt data : " + a.getFirst());
        // 반환된 변수의 타입 출력 
        System.out.println("  K Type : " + a.getFirst().getClass().getName());

        System.out.println("  second data : " + a.getSecond());
        // 반환된 변수의 타입 출력 
        System.out.println("  V Type : " + a.getSecond().getClass().getName());
    }
}

이렇게 외부 클래스에서 제네릭 클래스를 생성할 때 <> 괄호 안에 타입을 파라미터로 보내 제네릭 타입을 지정해주는것이 제네릭 프로그래밍이다.


3. 제네릭 메소드

  • 위 과정까지는 클래스 이름 옆에 라는 제네릭 타입을 붙여 해당 클래스 내에서 사용할 수 있는 E타입으로 일반화를 했다.

    • 그러나 그 외에 별도로 메소드에 한정한 제네릭도 사용할 수 있다.

메소드 선언 방법

public <T> T genericMethod(T o) {    // 제네릭 메소드
        ...
}
//[접근 제어자] <제네릭타입> [반환타입] [메소드명]([제네릭타입] [파라미터]) {
}

클래스는 클래스 명 옆에 제네릭 타입을 붙였지만, 메소드는 반환 타입 이전에 <> 제네릭 타입을 선언한다.

제네릭 클래스와 제네릭 메소드 함께 사용하는 예시

// 제네릭 클래스
class ClassName<E> {

    private E element;    // 제네릭 타입 변수

    void set(E element) {    // 제네릭 파라미터 메소드
        this.element = element;
    }

    E get() {    // 제네릭 타입 반환 메소드 
        return element;
    }

    <T> T genericMethod(T o) {    // 제네릭 메소드 >> 제네릭을 따로 주지않고 E유형을 가져올 수 있
        return o;
    }


}

public class Main {
    public static void main(String[] args) {

        ClassName<String> a = new ClassName<String>();
        ClassName<Integer> b = new ClassName<Integer>();

        a.set("10");
        b.set(10);

        System.out.println("a data : " + a.get());
        // 반환된 변수의 타입 출력 
        System.out.println("a E Type : " + a.get().getClass().getName());

        System.out.println();
        System.out.println("b data : " + b.get());
        // 반환된 변수의 타입 출력 
        System.out.println("b E Type : " + b.get().getClass().getName());
        System.out.println();

        // 제네릭 메소드 Integer
        System.out.println("<T> returnType : " + a.genericMethod(3).getClass().getName());

        // 제네릭 메소드 String
        System.out.println("<T> returnType : " + a.genericMethod("ABCD").getClass().getName());

        // 제네릭 메소드 ClassName b
        System.out.println("<T> returnType : " + a.genericMethod(b).getClass().getName());
    }
}

ClassName이란 객체를 생성할 때 <> 안에 파라미터를 지정한다.

  • a객체의 ClassName의 E제네릭 타입은 String으로 모두 변환된다.

  • b객체의 ClassName의 E제네릭 타입은 Integer로 모두 변환된다.

  • genericMethod()는 파라미터 타입에 따라 T 타입이 결정된다.

  • a는 String으로 객체를 생성했고, b는 Integer로 생성했는데, genericMehod를 보면 처음 생성할때 주었던 타입과 상관없이 별개로 타입을 줄 수 있는것이다.

    • 그래서 출력값을 보면 a는 String이지만, 해당 메소드는 별개의 타입을 가질 수 있기 때문에 파라미터로 받은 타입인 Integer 클래스라고 출력되는 것이고, 또 해당 메소드에 문자열로 인자값을 주니 String이라고 출려고된다.

즉, 클래스에서 지정한 제네릭 유형과 별도로 메소드에서 독립적으로 제네릭 유형을 선언하여 쓸 수 있다.


static 제네릭 메소드

class ClassName<E> {

    /*
     * 클래스와 같은 E 타입이더라도
     * static 메소드는 객체가 생성되기 이전 시점에
     * 메모리에 먼저 올라가기 때문에
     * E 유형을 클래스로부터 얻어올 방법이 없다.
     */
    static E genericMethod(E o) {    // error!
        return o;
    }

}

class Main {

    public static void main(String[] args) {

        // ClassName 객체가 생성되기 전에 접근할 수 있으나 유형을 지정할 방법이 없어 에러남
        ClassName.getnerMethod(3);

    }
}
  • 제네릭이 사용되는 메소드를 정적메소드로 두고 싶은 경우 >> 제네릭 클래스와 별도록 독립적인 제네릭이 사용되어야 한다.

예제

// 제네릭 클래스
class ClassName<E> {

    private E element; // 제네릭 타입 변수

    void set(E element) { // 제네릭 파라미터 메소드
        this.element = element;
    }

    E get() { // 제네릭 타입 반환 메소드
        return element;
    }

    // 아래 메소드의 E타입은 제네릭 클래스의 E타입과 다른 독립적인 타입이다.
    static <E> E genericMethod1(E o) { // 제네릭 메소드
        return o;
    }

    static <T> T genericMethod2(T o) { // 제네릭 메소드
        return o;
    }

}

public class Main {
    public static void main(String[] args) {

        ClassName<String> a = new ClassName<String>();
        ClassName<Integer> b = new ClassName<Integer>();

        a.set("10");
        b.set(10);

        System.out.println("a data : " + a.get());
        // 반환된 변수의 타입 출력
        System.out.println("a E Type : " + a.get().getClass().getName());

        System.out.println();
        System.out.println("b data : " + b.get());
        // 반환된 변수의 타입 출력
        System.out.println("b E Type : " + b.get().getClass().getName());
        System.out.println();

        // 제네릭 메소드1 Integer
        System.out.println("<E> returnType : " + ClassName.genericMethod1(3).getClass().getName());

        // 제네릭 메소드1 String
        System.out.println("<E> returnType : " + ClassName.genericMethod1("ABCD").getClass().getName());

        // 제네릭 메소드2 ClassName a
        System.out.println("<T> returnType : " + ClassName.genericMethod1(a).getClass().getName());

        // 제네릭 메소드2 Double
        System.out.println("<T> returnType : " + ClassName.genericMethod1(3.0).getClass().getName());
    }
}

제네릭 메소드는 제네릭 클래스 타입과 별도로 지정된다는 것을 볼 수 있다.

<>괄호 안에 타입을 파라미터로 보내 제네릭 타입을 지정해주는것.


제한된 Generic과 와일드 카드

만일 제네릭이 참조타입 모두가 되는 것이 아닌, 특정 범위 내로 좁혀서 제한하고 싶을 때 사용하는 방법

extends, super, ? >> ?는 와일드 카드로, '알 수 없는 타입'이라는 의미다.

<K extends T>    // T와 T의 자손 타입만 가능 (K는 들어오는 타입으로 지정 됨)
<K super T>    // T와 T의 부모(조상) 타입만 가능 (K는 들어오는 타입으로 지정 됨)

<? extends T>    // T와 T의 자손 타입만 가능
<? super T>    // T와 T의 부모(조상) 타입만 가능
<?>        // 모든 타입 가능. <? extends Object>랑 같은 의미
  • extends T: 상한 경계

  • ? super T : 하한 경계

  • : 와일드 카드

주의할점

  • K extens T? extends T 는 비슷한 구조지만 차이점이 있다.

    • k는 특정 타입으로 지정되지만, ? 는 타입이 지정되지 않는다는 의미다.
/*
 * Number와 이를 상속하는 Integer, Short, Double, Long 등의
 * 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 K는
 * 지정된 타입으로 변환이 된다.
 */
<K extends Number>


/*
 * Number와 이를 상속하는 Integer, Short, Double, Long 등의
 * 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 지정 되는 타입이 없어
 * 타입 참조를 할 수는 없다.
 */
<? extends T>    // T와 T의 자손 타입만 가능

특정 타입의 데이터를 조작하고자 할 경우에는 k같이 특정 제네릭 인수로 지정을 해주어야한다.


extends

상한 한계

  • extends뒤에 오는 타입이 최상위 타입으로 한계가 정해지는 것이다.

    • ex: Integer, Long, Byte등과 같은 수를 표현하는 래퍼 클래스만으로 제한하고 싶은 경우
    public class ClassName <K extends Number> { 
        ... 
    }
    
    public class Main {
        public static void main(String[] args) {
    
            ClassName<Double> a1 = new ClassName<Double>();    // OK!
    
            ClassName<String> a2 = new ClassName<String>();    // error!
        }
    }

    Double은 Number클래스를 상속받는 클래스라 가능하지만, String은 Number클래스와 별개의 클래스이기 때문에 에러를 띄운다.


super

하한 한계

  • super뒤에 오는 타입이 최하위 타입으로 한계가 정해진것으로, 부모(상위) 타입만 가능하다는 의미다.

    • 해당 객체가 업캐스팅(Up Casting)이 될 필요가 있을 때 사용한다.

      예로들어 '과일'이라는 클래스가 있고 이 클래스를 각각 상속받는 '사과'클래스와 '딸기'클래스가 있다고 가정해보자.

      이 때 각각의 사과와 딸기는 종류가 다르지만, 둘 다 '과일'로 보고 자료를 조작해야 할 수도 있다. (예로들면 과일 목록을 뽑는다거나 등등..)

      그럴 때 '사과'를 '과일'로 캐스팅 해야 하는데, 과일이 상위 타입이므로 업캐스팅을 해야한다. 이럴 때 쓸 수 있는 것이 바로 super라는 것이다.

    public class ClassName <E extends Comparable<? super E>> { ... }

PriorityQueue(우선순위 큐), TreeSet, TreeMap 같이 값을 정렬하는 클래스 만약 여러분이 특정 제네릭에 대한 자기 참조 비교를 하고싶을 경우 대부분 공통적으로 위와 같은 형식을 취한다.

  • 위 코드는 'E'가 Comparable인터페이스를 구현하는 타입 또는 E타입, E의 상위 클래스만을 허용하도록 제한하면서 >> Comparable인터페이스를 구현하지 않은 타입이 전달되는 것을 방지하며 코드의 안전성을 높인다.
public class SaltClass <E extends Comparable<E>> { ... }    // Error가능성 있음
public class SaltClass <E extends Comparable<? super E>> { ... }    // 안전성이 높음

public class Person {...}

public class Student extends Person implements Comparable<Person> {
    @Override
    public int compareTo(Person o) { ... };
}

public class Main {
    public static void main(String[] args) {
        SaltClass<Student> a = new SaltClass<Student>();
    }
}

<E extends Comparable<? super E>>

"E 자기 자신 및 조상 타입과 비교할 수 있는 E"


<?> 와일드 카드

<? extends Object> 와 동일하다.

Object는 자바에서의 모든 API 및 사용자 클래스의 최상위 타입이다.

public class ClassName { ... }
public class ClassName extends Object { ... } 

우리가 public class ClassName extends Object {} 를 묵시적으로 상속받는 것이나 다름이 없다.

  • 어떤 타입이든 상관 없다는 의미

Generic 클래스 단계별 문제

클래스명 뒤에 제네릭을 명시한다.

  1. Orange 클래스를 만들고 인스턴스 변수로는 당도를 둔다.

    (sugarContent) 당도를 초기화 하는 생성자를 만든다. 그리고 당도를 출력하는 메소드를 둔다.(showSugarContent).
    Orange 하나를 저장할 수 있는 OrangeBox를 만든다.(배열이 아닌 하나를 저장할 수 있게끔 한다.)
    Orange를 저장하고 꺼내는 메소드를 만든다.(store, pullOut)
    메인메소드에서 Orange의 당도가 10인 Orange를 OrangeBox에 저장한다.

    저장되어 있는 Orange를 꺼내서 당도를 확인한다.

        Apple 클래스를 만들고 인스턴스 변수로는 무게를 둔다.

        (weight) 무게를 초기화 하는 생성자를 만든다.

        그리고 무게를 출력하는 메소드를 둔다.(showAppleWeight)
Apple 하나를 저장할 수 있는 AppleBox를 만든다.

        (배열이 아닌 하나를 저장할 수 있게끔 한다.)
Apple를 저장하고 꺼내는 메소드를 만든다.(store, pullOut)
메인메소드에서 Apple의 무게가 200인 Apple을 AppleBox에 저장한다.

        저장되어 있는 Apple를 꺼내서 무게를 확인한다.

class Orange
{
    int sugarContent;    // 당분 함량
    public Orange(int sugar) { sugarContent=sugar; }
    public void showSugarContent()
    { 
        System.out.println("당도 "+sugarContent);
    }
}

class Apple
{
    int weight;    // 사과의 무게
    public Apple(int weight) { this.weight=weight; }
    public void showAppleWeight()
    { 
        System.out.println("무게 "+weight);
    }    
}
class OrangeBox
{
    Orange item;
    public void store(Orange item) { this.item=item; }
    public Orange pullOut() { return item; }
}

class AppleBox
{
    Apple item;
    public void store(Apple item) { this.item=item; }
    public Apple pullOut() { return item; }
}

class OrangeAppleMain
{    
    public static void main(String[] args)
    {
        OrangeBox oBox=new OrangeBox();
        oBox.store(new Orange(10)); //객체 생성 했으니까 
        Orange org1=oBox.pullOut(); //그 객체를 가지고 리턴받아서 그 값은 변수에 저장 
        org1.showSugarContent();//해당 변수에서 출력 

        AppleBox aBox=new AppleBox();
        aBox.store(new Apple(200));
        Apple app=aBox.pullOut();
        app.showAppleWeight();
    }
}

이건 완료


  1. 문제1의 소스코드를 OrangeBox와 AppleBox를 어떤 과일도 받을 수 있는 FruitBox로 바꾸어 본다.
    즉 두개의 클래스를 하나로 합친다. (제네릭 클래스를 사용하지 않는다.)
    메인메소드에서 Orange의 당도가 10인 Orange를 FruitBox에 저장한다.

    저장되어 있는 Orange를 꺼내서 당도를 확인한다.
    메인메소드에서 Apple의 무게가 200인 Apple을 FruitBox에 저장한다. 저장되어 있는 Apple를 꺼내서 무게를 확인한다.

class Orange
{
    int sugarContent;    // 당분 함량
    public Orange(int sugar) { sugarContent=sugar; }
    public void showSugarContent()
    { 
        System.out.println("당도 "+sugarContent);
    }
}

class Apple
{
    int weight;    // 사과의 무게
    public Apple(int weight) { this.weight=weight; }
    public void showAppleWeight()
    { 
        System.out.println("무게 "+weight);
    }    
}
class FruitBox
{
    Object item;
    public void store(Object item) { this.item=item; }
    public Object pullOut() { return item; }
}

class FruitBoxMain
{    
    public static void main(String[] args)
    {
        FruitBox fBox1=new FruitBox();
        fBox1.store(new Orange(10));
        Orange org1=(Orange)fBox1.pullOut();
        org1.showSugarContent();

        FruitBox fBox2=new FruitBox();
        fBox2.store(new Apple(200));
        Apple app=(Apple)fBox2.pullOut();
        app.showAppleWeight();
    }
}

Fruit 클래스를 만들어서 상속받아야 하나? 아니면 해당 메소드들을 다 다시 적어야하나..갈팡질팡 > 그리고 FruitBox에 생성자 만들어서..이걸 어떻게 넣지 하는 생각..


  1. 문제1과 문제2를 이상없이 풀었다는 과정하에 문제2에 있는 FruitBox에 문자열 "오렌지"를 저장하도록하고 컴파일 해보자.

    (새로운 소스코드를 작성해서 직접 실행해 본다.)

    어떤 현상이 벌어지는가? 예를 들어 소스파일 이름이 FruitBoxMain.java라고 하면 ,javac FruitBoxMain.java 라고 컴파일을 실행하면 컴파일 시에는 오류가 발생하지 않는다.

    하지만 java FruitBoxMain 하고서 프로그램을 실행시키게 되면 ClassCastException이 발생한다.

class Orange
{
    int sugarContent;    // 당분 함량
    public Orange(int sugar) { sugarContent=sugar; }
    public void showSugarContent()
    { 
        System.out.println("당도 "+sugarContent);
    }
}

class Apple
{
    int weight;    // 사과의 무게
    public Apple(int weight) { this.weight=weight; }
    public void showAppleWeight()
    { 
        System.out.println("무게 "+weight);
    }    
}
class FruitBox
{
    Object item;
    public void store(Object item) { this.item=item; }
    public Object pullOut() { return item; }
}

class FruitBoxMain
{    
    public static void main(String[] args)
    {
        FruitBox fBox1=new FruitBox();
        fBox1.store("오렌지");//컴파일 자체에서 오류 
        Orange org1=(Orange)fBox1.pullOut();
        org1.showSugarContent();

        FruitBox fBox2=new FruitBox();
        fBox2.store(new Apple(200));
        Apple app=(Apple)fBox2.pullOut();
        app.showAppleWeight();
    }
}

"오렌지" 할때 new로 객체를 생성했더니, 식별자 오류였나 떴었어서 뭐지..싶었다.

만약 저렇게 컴파일 하고싶다면 FruitBox를 제네릭으로 변환시켜서 했다면 컴파일 됐을것이다.

이렇듯 컴파일 과정에서 발견되는 오류는 매우 쉽게 해결이 가능하다. 반면 위에서 만든 예제와 같이 실행과정에서 발생하는 오류는 찾기가 쉽지 않다. 언뜻 보기에는 별 차이가 없어 보이지만, 프로그램의 규모가 크면 클수록 이 둘의 차이는 매우 극명하게 드러난다.

  • 실행과정에서 발견되는 오류를 컴파일 과정에서 발견되도록 코드를 작성하는 것은 매우 의미 있는 일이다.
  • 자료형에 대한 안전성이 보장된다.

  1. 문제1과 문제2를 이상없이 풀었다는 과정하에 문제1의 OrangeBox에 문자열 "오렌지"를 저장하도록하고 컴파일해보자.

    (새로운 소스코드를 작성해서 직접 실행해 본다.) 어떤 현상이 벌어지는가?

class Orange
{
    int sugarContent;    // 당분 함량
    public Orange(int sugar) { sugarContent=sugar; }
    public void showSugarContent()
    { 
        System.out.println("당도 "+sugarContent);
    }
}

class Apple
{
    int weight;    // 사과의 무게
    public Apple(int weight) { this.weight=weight; }
    public void showAppleWeight()
    { 
        System.out.println("무게 "+weight);
    }    
}
class OrangeBox
{
    Orange item;
    public void store(Orange item) { this.item=item; }
    public Orange pullOut() { return item; }
}

class AppleBox
{
    Apple item;
    public void store(Apple item) { this.item=item; }
    public Apple pullOut() { return item; }
}

class OrangeAppleMain
{    
    public static void main(String[] args)
    {
        OrangeBox oBox=new OrangeBox();
        oBox.store("오렌지");
        Orange org1=oBox.pullOut();
        org1.showSugarContent();

        AppleBox aBox=new AppleBox();
        aBox.store(new Apple(200));
        Apple app=aBox.pullOut();
        app.showAppleWeight();
    }
}

예를 들어 소스파일 이름이 OrangeAppleMain.java라고 하면,

javac OrangeAppleMain.java를 컴파일하면 컴파일 과정에서 오류가 난다.

자료형에 대한 안전성이 보장이 되지만 과일의 종류별로 상자 클래스를 만들어 주어야 하므로 소스코드의 양이 늘어나게 되고 유지보수가 어려워 진다.

  1. 문제2를 제네릭 클래스로 바꾼다.
class Orange
{
    int sugarContent;    // 당분 함량
    public Orange(int sugar) { sugarContent=sugar; }
    public void showSugarContent()
    { 
        System.out.println("당도 "+sugarContent);
    }
}

class Apple
{
    int weight;    // 사과의 무게
    public Apple(int weight) { this.weight=weight; }
    public void showAppleWeight()
    { 
        System.out.println("무게 "+weight);
    }    
}
class FruitBox<T> //보통 대문자 하나로 놓음 
{
    T item;
    public void store(T item) { this.item=item; }
    public T pullOut() { return item; }
}

class FruitBoxMain
{    
    public static void main(String[] args)
    {
        FruitBox<Orange> fBox1=new FruitBox<Orange>();
        fBox1.store(new Orange(10));
        Orange org1=fBox1.pullOut();
        org1.showSugarContent();

        FruitBox<Apple> fBox2=new FruitBox<Apple>();
        fBox2.store(new Apple(200));
        Apple app=fBox2.pullOut();
        app.showAppleWeight();
    }
}
  • FruitBox클래스는 타입 매개변수 T를 가지고 있는 것이다.

    • 이것은 FruitBox가 다양한 타입의 과일을 담을 수 있게 해준다.

      • 타입 매개변수 T는 저장하려는 과일의 타입을 나타낸다.

    여기서 T는 타입 매개변수로서, 모든 참조 타입을 대체할 수 있는 타입을 의미한다.

    FruitBox는 Orange타입의 객체만을 저장할 수 있게 되고, FruitBox은 Apple타입의 객체만을 저장할 수 있게 된다.


  1. 문제5를 이상없이 풀었다는 가정하에 FruitBox에 문자열 "오렌지"를 저장하도록하고 컴파일해보자.

    (새로운 소스코드를 작성해서 직접 실행해 본다.)

    어떤 현상이 벌어지는가?

    예를 들어 소스파일 이름이 FruitBoxMain.java라고 하면,

    javac FruitBoxMain.java를 컴파일하면 컴파일 과정에서 오류가 난다.

    자료형에 안정성도 보장이 되면서 어떤 과일도 저장할 수 있는 상자가 만들어 졌다.

class Orange
{
    int sugarContent;    // 당분 함량
    public Orange(int sugar) { sugarContent=sugar; }
    public void showSugarContent()
    { 
        System.out.println("당도 "+sugarContent);
    }
}

class Apple
{
    int weight;    // 사과의 무게
    public Apple(int weight) { this.weight=weight; }
    public void showAppleWeight()
    { 
        System.out.println("무게 "+weight);
    }    
}
class FruitBox<T>
{
    T item;
    public void store(T item) { this.item=item; }
    public T pullOut() { return item; }
}

class FruitBoxMain
{    
    public static void main(String[] args)
    {
        FruitBox<Orange> fBox1=new FruitBox<Orange>();
        fBox1.store("오렌지");
        Orange org1=fBox1.pullOut();
        org1.showSugarContent();

        FruitBox<Apple> fBox2=new FruitBox<Apple>();
        fBox2.store(new Apple(200));
        Apple app=fBox2.pullOut();
        app.showAppleWeight();
    }
}
  1. 예제 GenericBaseFruitBox.java의 FruitBox 클래스에 생성자를 추가하여, 다음의 main 메소드가 컴파일 및 실행됨을 확인해보자.

class GenericBaseFruitBox
{
public static void main(String[] args)
{
FruitBox orBox=new FruitBox(new Orange(10));
Orange org=orBox.pullOut();
org.showSugarContent();

            FruitBox apBox=new FruitBox(new Apple(20));
Apple app=apBox.pullOut();
app.showAppleWeight();
}

}

class Orange
{
    int sugarContent;    // 당분 함량
    public Orange(int sugar) { sugarContent=sugar; }
    public void showSugarContent()
    { 
        System.out.println("당도 "+sugarContent);
    }
}

class Apple
{
    int weight;    // 사과의 무게
    public Apple(int weight) { this.weight=weight; }
    public void showAppleWeight()
    { 
        System.out.println("무게 "+weight);
    }    
}

class FruitBox<T>
{
    T item;
    FruitBox(T item)
    {
        this.item = item;
    }
    public void store(T item) { this.item=item; }
    public T pullOut() { return item; }
}

class GenericBaseFruitBox
{    
    public static void main(String[] args)
    {
        FruitBox<Orange> orBox=new FruitBox<Orange>(new Orange(10));
        Orange org=orBox.pullOut();
        org.showSugarContent();

        FruitBox<Apple> apBox=new FruitBox<Apple>(new Apple(200));
        Apple app=apBox.pullOut();
        app.showAppleWeight();
    }
}

제네릭 클래스의 생성자를 만드는것은 우리가 아는 생성자 만드는 법과 같다.


Generic 메소드 단계별 문제

반환하는 자료형 앞에 제네릭을 명시한다.

  1. AAA 클래스에 매개변수가 한개인 제네릭 메소드를 정의하고 main 메소드가 있는 Test 클래스에서 호출해 보자.

  2. 둘 이상의 매개변수를 받는 제네릭 메소드를 정의하고 호출해 보자.

package com.test.memo;

class AAA {

    <A> void method(A num) {
        System.out.println(num);
    }

    <T, U> void method2(T num1, U num2) {
        System.out.println(num1 + " " + num2);
    }
}

public class Practice2 {
    public static void main(String[] args) {
        AAA a = new AAA();
        a.method(10);

        a.method2("hi", 123);
    }
}

오버로딩 해서 이렇게 간단하게 해도 될듯 > 귀찮아서,,

package com.test.memo;

class AAA {
    <T> void method(T param) {
        System.out.println(param);
    }

    <T, N> void method(T param, N num) {
        System.out.println(param);
        System.out.println(num);
    }
}

public class Practice2 {

    public static void main(String[] args) {
        AAA aaa = new AAA();
        aaa.method(10);
        aaa.method("하이", 20);

    }
}
  1. 둘 이상의 인스턴스 변수 기반의 제네릭 클래스를 정의해 보자.
package com.test.memo;

class AAA<S, I> {
    private S str;
    private I it;

    AAA(S str, I it) {
        this.str = str;
        this.it = it;
    }

    S getS() {
        return str;
    }

    I getI() {
        return it;
    }

}

public class Practice2 {
    public static void main(String[] args) {
        AAA aaa = new AAA("스트링", 10);
        System.out.println(aaa.getI());
        System.out.println(aaa.getS());

    }
}
  1. 아래에 정의된 클래스는 컴파일 시 에러가 발생한다. 여러분이 직접 컴파일을 해서 문제점이 무엇인지 확인하고, 문제점의 원인을 유추하기 바란다.
class MyClass
{
    public <T> void simpleMethod(T param)
    {
        param.showData();
        System.out.println(param);
    }
}
  • 제네릭은 일반화를 시키는 용도인데, 모든 클래스가 showData()라는 메소드를 가지고 있는지 모르기 때문에 오류가 나는 것이다. >

    • 그러므로 Object클래스 안에있는 메소드를 저렇게 호출하는 것이 가능하다.

  1. 문제5폴더에 있는 소스코드는 컴파일시 에러가 발생한다. 이것을 에러가 없게 수정하자.
    Generic 부분은 수정하지 않고 메소드의 몸통만 수정해서 에러가 없게 하자.(showInstanceAncestor, showInstanceName 메소드의 내용부분)

6번의 예제처럼 나왔어야하는데 정답 겸 7번의 정답이 되어버렸다. 그냥 앞으로 제네릭 쓸때 이렇게 쓰면 된당!

package com.test.memo;

interface SimpleInterface {
    public void showYourName();
}

class UpperClass {
    public void showYourAncestor() {
        System.out.println("UpperClass");
    }
}

class AAA extends UpperClass implements SimpleInterface {
    public void showYourName() {
        System.out.println("Class AAA");
    }
}

class BBB extends UpperClass implements SimpleInterface {
    public void showYourName() {
        System.out.println("Class BBB");
    }
}

public class Practice2 {
    public static <T extends UpperClass> void showInstanceAncestor(T param) {
        param.showYourAncestor();
    }

    public static <T extends SimpleInterface> void showInstanceName(T param) {
        param.showYourName();
    }

    public static void main(String[] args) {
        AAA aaa = new AAA();
        BBB bbb = new BBB();

        showInstanceAncestor(aaa);
        showInstanceName(aaa);
        showInstanceAncestor(bbb);
        showInstanceName(bbb);
    }
}
  • 메소드에서 로만 넣어두면 해당 자료형에 호출하고 있는 메소드들이 존재하는지 모르기 때문에, 해당 메소드를 추상메소드로 두고 있는 클래스를 상속받도록 제네릭을 변경시켰다.

    • showYourAncestor(); 는 UpperClass의 메소드이고, showYourName();은 SimpleInterface 인터페이스의 메소드 이므로 해당 클래스와 인터페이스를 상속 및 구현해줬다.

      • 제네릭에서는 상속과 구현 모두 extends를 이용한다.
  1. (문제 아님)
interface SimpleInterface
{
    public void showYourName();
}

class UpperClass
{
    public void showYourAncestor()
    {
        System.out.println("UpperClass");
    }
}

class AAA implements SimpleInterface
{
    public void showYourName() 
    {
        System.out.println("Class AAA");
    }
}

class BBB extends UpperClass
{
}



class BoundedTypeParam
{    
    public static <T> void showInstanceAncestor(T param)
    {
        ((UpperClass)param).showYourAncestor();

    }

    public static <T> void showInstanceName(T param)
    {
        ((SimpleInterface)param).showYourName();
    }

    public static void main(String[] args)
    {
        AAA aaa=new AAA();
        BBB bbb=new BBB();

        showInstanceAncestor(aaa);
        showInstanceName(bbb);
    }
}

컴파일 시에는 에러가 발생하지 않지만(javac BoundedTypeParam.java) 런타임시에는 오류가 발생한다.

  1. 제네릭 매개변수로는 object클래스에 정의된 메소드만 호출 가능하기 때문에, 아래 예제에서는 매개변수 param을 강제 형변환하고 있다. > 그러나 안전하지 않음

    • SimpleInterface 인터페이스를 구현하지 않은 인스턴스, 또는 UpperClass를 상속하지 않은 인스턴스의 참조 값이 메소드에 전달되어도 컴파일 및 실행이 되기 때문에, 제네릭의 장점이 소멸되어 버린다.

6-1을 안정성 있는 형태로

interface SimpleInterface
{
    public void showYourName();
}

class UpperClass
{
    public void showYourAncestor()
    {
        System.out.println("UpperClass");
    }
}

class AAA extends UpperClass
{
}

class BBB implements SimpleInterface
{
    public void showYourName() 
    {
        System.out.println("Class BBB");
    }
}

class BoundedTypeParam
{    
    public static <T> void showInstanceAncestor(T param)
    {
        ((SimpleInterface)param).showYourName();
    }

    public static <T> void showInstanceName(T param)
    {
        ((UpperClass)param).showYourAncestor();
    }

    public static void main(String[] args)
    {
        AAA aaa=new AAA();
        BBB bbb=new BBB();

        showInstanceAncestor(aaa);
        showInstanceName(bbb);
    }
}
  • main 클래스에서는 같지만, 다른 클래스에서의 상속과 메소드가 다르다.

0개의 댓글