😥 기본형의 한계

자바에서 기본형은 객체가 아니기 때문에 객체 지향 프로그래밍의 장점을 살릴 수 없다. 메서드도 사용할 수 없고, 객체 참조가 필요한 컬렉션 프레임워크도 사용할 수 없다. 그리고 제네릭도 사용할 수 없다. "그리고 무엇보다 null 값을 가질 수 없다는 점이 경우에 따라 곤란할 수 있다." 기본형은 항상 값을 초기화 해줘야 하기 때문에 null 값처럼 “데이터가 없음” 이라는 상태를 나타낼 방법이 없다. 아래 코드를 보자.

package lang.wrapper;

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

        int value = 10;
        int i1 = compareTo(value, 5);
        int i2 = compareTo(value, 10);
        int i3 = compareTo(value, 20);
        System.out.println("i1 = " + i1);
        System.out.println("i2 = " + i2);
        System.out.println("i3 = " + i3);

    }

    public static int compareTo(int value, int target) {
        if (value < target) {
            return -1;
        } else if (value > target) {
            return 1;
        } else {
            return 0;
        }
    }
}

/*
i1 = 1
i2 = 0
i3 = -1
*/

이 간단한 예제를 왜 만들었을까? 위의 value와 비교 대상 값을 compareTo()라는 외부 메서드를 사용해서 비교한다. 근데 자기 자신의 value와 다른 값을 연산하는 것이기 때문에 항상 자기 자신의 값인 value가 사용된다. 이런 경우, value를 객체로 만들면 value 객체 스스로 자신의 값과 다른 값을 비교하도록 하면 되는데 value가 현재 기본형이기에 value.compareTo()처럼 메서드를 만들 수 없다.

 

🗞️ 직접 만든 Wrapper 클래스

int를 클래스로 만들어보자. int는 클래스가 아니지만, int 값을 가지고 클래스를 만들면 된다. 특정 기본형을 감싸서 클래스를 만들면 된다.

package lang.wrapper;

public class MyInteger {

    private final int value;

    public MyInteger(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    // 나의 값과 넘어온 값을 비교
    public int compareTo(int target) {
        if (value < target) {
            return -1;
        } else if (value > target) {
            return 1;
        } else {
            return 0;
        }
    }

    @Override
    public String toString() {
        return Integer.toString(value);  // 숫자를 문자로 변경
    }
}

위의 코드를 보면, MyInteger 클래스는 value라는 단순한 기본형 변수를 하나 가지고 있고 불변으로 설계되었다. value를 편리하게 사용할 수 있도록 다양한 메서드도 제공하고 있다. 앞에서 똑 떨어져 있었던 compareTo() 메서드를 인스턴스에 소속된 메서드로, 즉 클래스 내부로 캡슐화 한 것이다. 이제 데이터 쪼가리에 불과했던 valueMyInteger 클래스를 통해 객체로 다룰 수 있게 된 것이다.

package lang.wrapper;

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

        MyInteger myInteger = new MyInteger(10);
        int i1 = myInteger.compareTo(5);
        int i2 = myInteger.compareTo(10);
        int i3 = myInteger.compareTo(20);

        System.out.println("i1 = " + i1);
        System.out.println("i2 = " + i2);
        System.out.println("i3 = " + i3);
    }
}

/*
i1 = 1
i2 = 0
i3 = -1
*/

보다시피, myInteger.compareTo()는 자기 자신의 값을 외부의 값을 비교한다. MyInteger는 객체이기 떄문에 본인의 메서드를 호출할 수 있다.

 

이제 다른 중요한 얘기로 넘어가보자. 아까 말했다시피 기본형은 항상 값을 가져야 한다. 지역 변수도 값을 초기화해야 하고, 멤버 변수로 사용해도 기본값을 항상 0으로 초기화해야 한다. 하지만 때로는 데이터가 “없음” 이라는 상태가 필요할 수도 있다.

package lang.wrapper;

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

        int[] intArr = {-1, 0, 1, 2, 3};

        System.out.println(findValue(intArr, -1));
        System.out.println(findValue(intArr, 0));
        System.out.println(findValue(intArr, 1));
        System.out.println(findValue(intArr, 100));

        // -1을 찾은 경우와 찾는 수가 배열에 없는 경우, 반환되는 값이 같다...

    }

    private static int findValue(int[] arr, int target) {
        for (int i : arr) {
            if (i == target) {
                return i;
            }
        }

        // 반환 타입이 int, 즉 기본형이기 때문에 뭐라도 반환해야 한다.
        return -1;
    }
}

/*
-1
0
1
-1
*/

위의 코드를 보면, findValue() 메서드는 배열에 찾는 값이 있으면 해당 값을 반환하고, 그렇지 않다면 -1을 반환하도록 설계되었다. 보다시피 바로 문제점이 보인다. 해당 값을 찾았는데 그 값이 -1인 경우와, 그렇지 않았을 때 반환된 -1을 구분할 방법이 없다. 다른 값으로 반환한다고 해도 상황은 변하지 않을 것이다. 이처럼 기본형은 항상 값을 반환한다는 점 때문에 문제가 발생한 것이다. 이 코드를 객체로 다시 설계해보도록 하자.

package lang.wrapper;

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

        MyInteger[] intArr = {new MyInteger(-1), new MyInteger(0), new MyInteger(1)};

        System.out.println(findValue(intArr, -1));
        System.out.println(findValue(intArr, 0));
        System.out.println(findValue(intArr, 1));
        System.out.println(findValue(intArr, 100));

    }

    private static MyInteger findValue(MyInteger[] arr, int target) {
        for (MyInteger myInteger : arr) {
            if (myInteger.getValue() == target) {
                return myInteger;
            }
        }

        return null;
    }
}

/*
-1
0
1
null
*/

보다시피 객체에는 데이터가 없다는 null라는 명확한 값이 존재한다. 실행 결과를 보면 -1을 입력했을 때는 -1을 반환하고, 값을 찾지 못했을 경우에는 null을 반환하는 것을 볼 수 있다. 이게 바로 Wrapper 클래스의 필요성이다. 하지만 null 값을 반환하는 경우, 잘못하면 NPE가 터질 수도 있기 때문에 각별한 주의가 필요하다.


🎁 Wrapper 클래스 - 자바 래퍼 클래스

Wrapper 클래스는 기본형을 객체로 만든 버전이라고 생각하면 된다. 자바는 기본형에 대응하는 Wrapper 클래스를 기본으로 제공한다. 기본 Wrapper 클래스는 불변이고, equals() 메서드로 비교해야 한다는 특징이 있다.

  • byteByte
  • shortShort
  • intInteger
  • longLong
  • floatFloat
  • doubleDouble
  • charCharacter
  • booleanBoolean

 

실제 사용법을 보도록 하자.

package lang.wrapper;

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

        Integer newInteger = new Integer(10);  // 미래에 삭제 예정, 대신 valueOf() 사용
        Integer integerObj = Integer.valueOf(10);  // -128 ~ 127 자주 사용하는 숫자값 재사용, 불변
        Long longObj = Long.valueOf(100);
        Double doubleObj = Double.valueOf(10.5);

        System.out.println("newInteger = " + newInteger);
        System.out.println("integerObj = " + integerObj);
        System.out.println("longObj = " + longObj);
        System.out.println("doubleObj = " + doubleObj);

        System.out.println("--내부 값 읽기--");
        int intValue = integerObj.intValue();
        System.out.println("intValue = " + intValue);
        long longValue = longObj.longValue();
        System.out.println("longValue = " + longValue);

        System.out.println("--비교--");
        System.out.println("==: " + (newInteger == integerObj));
        System.out.println("equals: " + newInteger.equals(integerObj));
    }
}

/*
newInteger = 10
integerObj = 10
longObj = 100
doubleObj = 10.5
--내부 값 읽기--
intValue = 10
longValue = 100
--비교--
==: false
equals: true
*/

 

<래퍼 클래스 생성 - 박싱(Boxing)>

  • 기본형을 래퍼 클래스로 변경하는 것을 마치 박스에 물건을 넣은 것 같다고 해서 박싱(Boxing)이라 한다.
  • new Integer(10)은 직접 사용하면 안된다. 작동은 하지만, 향후 자바에서 제거될 예정이다.
  • 대신에 Integer.valueOf(10)를 사용하면 된다.
    • 내부에서 new Integer(10)을 사용해서 객체를 생성하고 돌려준다.
  • 추가로 Integer.valueOf()에는 성능 최적화 기능이 있다. 개발자들이 일반적으로 자주 사용하는 -128 ~ 127 범위의 Integer 클래스를 미리 생성해준다. 해당 범위의 값을 조회하면 미리 생성된 Integer 객체를 반환한다. 해당 범위의 값이 없으면 new Integer() 를 호출한다.
    • 마치 문자열 풀과 비슷하게 자주 사용하는 숫자를 미리 생성해두고 재사용한다.
    • 참고로 이런 최적화 방식은 미래에 더 나은 방식으로 변경될 수 있다.

 

<intValue() - 언박싱(Unboxing)>

  • 래퍼 클래스에 들어있는 기본형 값을 다시 꺼내는 메서드이다.
  • 박스에 들어있는 물건을 꺼내는 것 같다고 해서 언박싱(Unboxing)이라 한다.

📦 오토 박싱, 오토 언박싱

자바에서 intInteger로 변환, Integerint로 변환하는 부분을 정리해보자. 아래와 같이 valueOf(), intValue() 메서드를 사용하면 된다.

package lang.wrapper;

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

        // Primitive -> Wrapper (박싱)
        int value = 7;
        Integer boxedValue = Integer.valueOf(value);

        // Wrapper -> Primitive (언박싱)
        int unboxedValue = boxedValue.intValue();

        System.out.println("boxedValue = " + boxedValue);
        System.out.println("unboxedValue = " + unboxedValue);
    }
}

/*
boxedValue = 7
unboxedValue = 7
*/

Boxing 할 때는 valueOf()를, Unboxing 할 때는 위와 같이 intValue()처럼 타입에 맞게 xxxValue() 형식으로 써주면 된다. 하지만 이런 기본형과 래퍼 클래스 간의 변환이 자주 발생했기 때문에 많은 개발자들이 불편함을 호소했다. 자바는 이런 문제를 해겷기 위해 오토 박싱(Auto-Boxing), 오토 언박싱(Auto-Unboxing)을 지원한다.

package lang.wrapper;

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

        // Primitive -> Wrapper
        int value = 7;
        Integer boxedValue = value;  // Auto-Boxing, valueOf() 메서드 안 써도 됨.

        // Wrapper -> Primitive
        int unboxedValue = boxedValue;  // Auto-Unboxing, intValue() 메서드 안 써도 됨.

        System.out.println("boxedValue = " + boxedValue);
        System.out.println("unboxedValue = " + unboxedValue);
    }
}

/*
boxedValue = 7
unboxedValue = 7
*/

위와 같이 오토 박싱과 오토 언박싱은 컴파일러가 개발자 대신 valueOf(), xxxValue() 등의 코드를 추가해주는 기능이다. 덕분에 기본형과 래퍼형을 서로 편리하게 변환할 수 있게 되었다.


🛠️ Wrapper 클래스의 주요 메서드와 성능

Wrapper 클래스가 제공하는 주요 메서드를 알아보도록 하자.

package lang.wrapper;

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

        Integer i1 = Integer.valueOf(10);  // 숫자를 래퍼 객체로 변환 가능
        Integer i2 = Integer.valueOf("10");  // 문자열을 래퍼 객체로 변환하는 것도 가능
        int intValue = Integer.parseInt("10");

        // 비교
        int compareResult = i1.compareTo(20);  // 내 값이 더 작으니까 -1 출력
        System.out.println("compareResult = " + compareResult);

        // 산술 연산
        System.out.println("sum: " + Integer.sum(10, 20));
        System.out.println("min: " + Integer.min(10, 20));
        System.out.println("max: " + Integer.max(10, 20));
    }
}

/*
compareResult = -1
sum: 30
min: 10
max: 20
*/
  • valueOf(): 래퍼 타입을 반환한다. 숫자, 문자열을 모두 지원한다.
  • parseInt(): 문자열을 기본형으로 변환한다.
  • compareTo(): 내 값과 인수로 넘어온 값을 비교한다. 내 값이 크면 1 , 같으면 0 , 내 값이 작으면 -1 을 반환한다.
  • Integer.sum() , Integer.min() , Integer.max(): static 메서드이다. 간단한 덧셈, 작은 값, 큰값 연산을 수행한다.

 

🤔 그럼 parseInt() vs valueOf() 둘 중 뭘 써야 하는데?

그냥 원하는 타입에 맞는 메서드를 사용하면 된다. valueOf()Wrapper 타입을 반환하고, parseInt()는 기본형을 반환한다.

Wrapper 클래스는 객체이기 때문에 기본형보다 다양한 기능을 제공한다. 그럼 다 Wrapper 클래스만 쓰면 되는거 아닌가? 왜 자바는 기본형도 제공하는거지? 다 이유가 있다… 아래 코드를 살펴보자.

package lang.wrapper;

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

        int iterations = 1_000_000_000;  // 반복 횟수를 10억으로 설정
        long startTime, endTime;

        // 기본형 long 사용
        long sumPrimitive = 0;
        startTime = System.currentTimeMillis();
        for (int i = 0; i < iterations; i++) {
            sumPrimitive += i;
        }

        endTime = System.currentTimeMillis();
        System.out.println("sumPrimitive = " + sumPrimitive);
        System.out.println("기본 자료형 long 실행 시간: " + (endTime - startTime) + "ms");

        /*
        sumPrimitive = 499999999500000000
        기본 자료형 long 실행 시간: 530ms
        */

        // 래퍼 클래스 Long 사용
        Long sumWrapper = 0L;
        startTime = System.currentTimeMillis();
        for (int i = 0; i < iterations; i++) {
            sumWrapper += i;
        }

        endTime = System.currentTimeMillis();
        System.out.println("sumWrapper = " + sumWrapper);
        System.out.println("Wrapper 클래스 Long 실행 시간: " + (endTime - startTime) + "ms");

        /*
        sumWrapper = 499999999500000000
        Wrapper 클래스 Long 실행 시간: 1292ms
        */
    }
}

단순히 값을 반복해서 10억 번을 더하는 코드다. 결과는 같지만, 연산 속도에 확연한 차이가 있는 것을 볼 수 있다. "기본형 연산이 Wrapper 클래스보다 훨씬 빠르다." 기본형은 메모리에서 단순히 그 크기만큼의 공간을 차지한다. 반면, Wrapper 클래스의 인스턴스는 내부에 필드로 가지고 있는 기본형의 값 뿐만 아니라 자바에서 객체 자체를 다루는데 필요한 객체 메타데이터를 포함하므로 더 많은 메모리를 사용한다. 자바 버전과 시스템마다 다르지만 대략 8~16byte의 메모리를 추가 사용한다.

하지만, 지금이야 10억 번을 돌리니까 차이가 커 보이지만 사실 1회로 환산하면 둘 다 매우 빠르게 연산되는 것이다. 일반적인 애플리케이션을 만든다고 한다면, 이런 부분을 최적화해도 간에 기별도 가지 않는다. CPU 연산을 아주 많이 수행하는 특수한 경우, 혹은 수만, 수십만 이상 연속해서 연산을 수행하는 경우라면 기본형을 사용해서 최적화를 고려하도록 하자. 그렇지 않은 경우라면 코드를 유지보수하기 더 나은 것을 선택하면 된다.

 

🤔 유지보수 vs 최적화

유지보수와 최적화를 고려해야 하는 상황이라면 유지보수를 먼저 고려해야 한다. 특히 최신 컴퓨터는 매우 빠르기 때문에 메모리 상에서 발생하는 연산을 몇 번 줄인다고해도 실질적인 도움이 되지 않는 경우가 많다. 코드 변경 없이 성능 최적화를 하면 가장 좋겠지만, 성능 최적화는 대부분 단순함 보다는 복잡함을 요구하고, 더 많은 코드들을 추가로 만들어야 한다. 최적화를 위해 유지보수 해야 하는 코드가 더 늘어나는 것이다. 그런데 진짜 문제는 최적화를 한다고 했지만 전체 애플리케이션의 성능 관점에서 보면 불필요한 최적화를 할 가능성이 있다. 특히 웹 애플리케이션의 경우 메모리 안에서 발생하는 연산 하나보다 네트워크 호출 한 번이 많게는 수십만 배 더 오래 걸린다. 자바 메모리 내부에서 발생하는 연산을 수천번에서 한 번으로 줄이는 것 보다, 네트워크 호출 한 번을 더 줄이는 것이 더 효과적인 경우가 많다. 권장하는 방법은 개발 이후에 성능 테스트를 해보고 정말 문제가 되는 부분을 찾아서 최적화 하는 것이다.


🔬 Class 클래스

자바에서 Class 클래스를 통해 개발자는 실행 중인 자바 애플리케이션 내에서 필요한 클래스의 속성과 메서드에 대한 정보를 조회하고 조작할 수 있다.

 

Class 클래스의 주요 기능은 아래와 같다.

  • 타입 정보 얻기: 클래스의 이름, 슈퍼클래스, 인터페이스, 접근 제한자 등과 같은 정보를 조회할 수 있다.

  • 리플렉션: 클래스에 정의된 메서드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메서드를 호출하는 등의 작업을 할 수 있다.

  • 동적 로딩과 생성: Class.forName() 메서드를 사용하여 클래스를 동적으로 로드하고, newInstance()메서드를 통해 새로운 인스턴스를 생성할 수 있다.

  • 애노테이션 처리: 클래스에 적용된 애노테이션(annotation)을 조회하고 처리하는 기능을 제공한다.

 

아래 코드를 살펴보자.

package lang.clazz;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ClassMetaMain {
    public static void main(String[] args) throws Exception {

        // Class 조회
        Class clazz = String.class;  // 1. 클래스에서 조회
        // Class clazz = new String().getClass(); 2. 이런 식으로 인스턴스에서 조회할 수도 있음
        // Class clazz = Class.forName("java.lang.String"); 3. 문자열로 조회할 수도 있음

        // 모든 필드 출력
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            System.out.println("Field = " + field.getType() + " " + field.getName());
        }

        // 모든 메서드 출력
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            System.out.println("Method = " + method);
        }

        // 상위 클래스 정보 출력
        System.out.println("Superclass: " + clazz.getSuperclass().getName());

        // 인터페이스 정보 출력
        Class[] interfaces = clazz.getInterfaces();
        for (Class anInterface : interfaces) {
            System.out.println("Interface = " + anInterface.getName());
        }
    }
}

class vs clazz: class는 자바의 예약어다. 따라서 패키지명, 변수명으로 사용할 없다. 이런 이유로 자바 개발자들은 class 대신 clazz라는 이름을 관행으로 사용한다. clazzclass와 유사하게 들리고, 이 단어가 class를 의미한다는 것을 쉽게 알 수 있다.

 

Class 클래스는 엄청 많지만 주요 기능만 살펴보자면…

  • getDeclaredFields(): 클래스의 모든 필드를 조회한다.
  • getDeclaredMethods(): 클래스의 모든 메서드를 조회한다.
  • getSuperclass(): 클래스의 부모 클래스를 조회한다.
  • getInterfaces(): 클래스의 인터페이스들을 조회한다.

실행해보면, String 클래스에 대한 아주 많은 정보를 확인할 수 있다.

 

🪜 클래스 생성하기

Class 클래스로 인터페이스를 만들어보자.

package lang.clazz;

public class Hello {

    public String hello() {
        return "Hello!";
    }
}
package lang.clazz;

public class ClassCreateMain {
    public static void main(String[] args) throws Exception {

        // Class helloClass = Hello.class;
        Class helloClass = Class.forName("lang.clazz.Hello");
        Hello hello = (Hello) helloClass.getDeclaredConstructor().newInstance();
        String result = hello.hello();

        System.out.println("hello = " + hello);  // 객체 생성됨
        System.out.println("result = " + result);
    }
}

/*
hello = lang.clazz.Hello@a09ee92
result = Hello!
*/

getDeclaredConstructor().newInstance()의 의미는 생성자를 선택해서 그걸 기반으로 인스턴스를 생성하라는 의미다. 실제 출력해보면, 인스턴스의 참조값이 출력되는 것을 확인할 수 있다.

 

<리플렉션 - reflection>

Class를 사용하면 클래스의 메타 정보를 기반으로 클래스에 정의된 메서드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메서드를 호출하는 작업을 할 수 있다. 이런 작업을 리플렉션이라 한다. 추가로 애노테이션 정보를 읽어서 특별한 기능을 수행할 수 도 있다. 최신 프레임워크들은 이런 기능을 적극 활용한다. 지금은 그냥 Class라는게 있고, 이걸 이용하면 메타정보를 얻을 수 있구나 정도만 이해하고 넘어가도록 하자.


⚙️ System 클래스

System 클래스는 시스템과 관련된 기본 기능들을 제공한다.

package lang.system;

import java.util.Arrays;

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

        // 현재 시간(밀리초)를 가져온다.
        long currentTimeMills = System.currentTimeMillis();
        System.out.println("currentTImeMills = " + currentTimeMills);

        // 현재 시간(나노초)를 가져온다.
        long currentTimeNano = System.nanoTime();
        System.out.println("currentTimeNano = " + currentTimeNano);

        // 환경 변수를 읽는다.
        System.out.println("getenv = " + System.getenv());

        // 시스템 속성을 읽는다.
        System.out.println("properties = " + System.getProperties());
        System.out.println("Java version: " + System.getProperty("java.version"));

        // 배열을 고속으로 복사
        char[] originalArray = new char[]{'h', 'e', 'l', 'l', 'o'};
        char[] copiedArray = new char[originalArray.length];
        System.arraycopy(originalArray, 0, copiedArray, 0, originalArray.length);

        // 배열 출력
        System.out.println("copiedArray = " + copiedArray);
        System.out.println("Arrays.toString = " + Arrays.toString(copiedArray));

        System.exit(0);
    }
}
  • 표준 입력, 출력, 오류 스트림: System.in, System.out, System.err은 각각 표준 입력, 표준 출력, 표준 오류 스트림을 나타낸다.

  • 시간 측정: System.currentTimeMillis()System.nanoTime()은 현재 시간을 밀리초 또는 나노초단위로 제공한다.

  • 환경 변수: System.getenv() 메서드를 사용하여 OS에서 설정한 환경 변수의 값을 얻을 수 있다.

  • 시스템 속성: System.getProperties()를 사용해 현재 시스템 속성을 얻거나 System.getProperty(String key)로 특정 속성을 얻을 수 있다. 시스템 속성은 자바에서 사용하는 설정값이다.

  • 시스템 종료: System.exit(int status) 메서드는 프로그램을 종료하고, OS에 프로그램 종료의 상태 코드를 전달한다.

    • 상태 코드 0 : 정상 종료
    • 상태 코드 0이 아님: 오류나 예외적인 종료
  • 배열 고속 복사: System.arraycopy 는 시스템 레벨에서 최적화된 메모리 복사 연산을 사용한다. 직접 반복문을 사용해서 배열을 복사할 때 보다 수 배 이상 빠른 성능을 제공한다. 그냥 originalArrays를 통째로 읽어서 copiedArray에 복사하는 것이다.


🧮 Math, Random 클래스

Math 클래스는 수학 문제를 해결해주는 클래스로, 너무 많은 기능을 제공하기 때문에 어떤 것들이 있는지 확인만 하고 넘어가자. 필요한 메서드는 그때 그때 찾아 쓰자.

  1. 기본 연산 메서드
  • abs(x): 절대값
  • max(a, b): 최대값
  • min(a, b): 최소값
  1. 지수 로그 연산 메서드
  • exp(x): e^x 계산
  • log(x): 자연 로그
  • log10(x): 로그 10
  • pow(a, b): a의 b 제곱
  1. 반올림 정밀도 메서드
  • ceil(x): 올림
  • floor(x): 내림
  • rint(x): 가장 가까운 정수로 반올림
  • round(x): 반올림
  1. 삼각 함수 메서드
  • sin(x): 사인
  • cos(x): 코사인
  • tan(x): 탄젠트
  1. 기타 유용한 메서드
  • sqrt(x): 제곱근
  • cbrt(x): 세제곱근
  • random(): 0.0과 1.0 사이의 무작위 값 생성

 

자주 사용되는 기능들만 예제로 만들어서 실행해보도록 하자.

package lang.math;

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

        // 기본 연산 메서드
        System.out.println("max(10, 20): " + Math.max(10, 20));
        System.out.println("min(10, 20): " + Math.min(10, 20));
        System.out.println("abs(-10): " + Math.abs(-10));

        // 반올림 및 정밀도 메서드
        System.out.println("ceil(2.1): " + Math.ceil(2.1));
        System.out.println("floor(2.7): " + Math.floor(2.7));
        System.out.println("round(2.5): " + Math.round(2.5));

        // 기타 유용한 메서드
        System.out.println("sqrt(4): " + Math.sqrt(4));
        System.out.println("random(): " + Math.random()); // 0.0 ~ 1.0 사이의 double 값 반환
    }
}

/*
max(10, 20): 20
min(10, 20): 10
abs(-10): 10
ceil(2.1): 3.0
floor(2.7): 2.0
round(2.5): 3
sqrt(4): 2.0
random(): 0.7507946707823384
*/

Random 클래스

Math 클래스에도 random() 메서드가 있지만, Random 클래스를 사용하면 더욱 다양한 랜덤값을 구할 수 있다. 참고로 Math.random()도 내부에서는 Random 클래스를 사용하고 있다.

package lang.math;

import java.util.Random;

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

        Random rand = new Random();
        // Random rand = new Random(1);  // seed가 같으면 실행을 반복해 결과가 계속 동일하다.

        int randomInt = rand.nextInt();
        System.out.println("randomInt = " + randomInt);

        double randomDouble = rand.nextDouble();
        System.out.println("randomDouble = " + randomDouble);

        boolean randomBoolean = rand.nextBoolean();
        System.out.println("randomBoolean = " + randomBoolean);

        // 범위 조회
        int randomRange1 = rand.nextInt(10);
        System.out.println("0 ~ 9: " + randomRange1);

        int randomRange2 = rand.nextInt(10) + 1;
        System.out.println("1 ~ 10: " + randomRange2);
    }
}

/*
randomInt = -953326647
randomDouble = 0.1556065348287149
randomBoolean = false
0 ~ 9: 6
1 ~ 10: 8
*/

당연히 실행 결과는 항상 다르다. 위의 메서드를 분석해보자면

  • random.nextInt(): 랜덤 int 값을 반환한다.
  • nextDouble(): 0.0d ~ 1.0d 사이의 랜덤 double 값을 반환한다.
  • nextBoolean(): 랜덤 boolean 값을 반환한다.
  • nextInt(int bound): 0 ~ bound 미만의 숫자를 랜덤으로 반환한다. 예를 들어서 3을 입력하면 0, 1, 2 를 반환한다.

1부터 특정 숫자의 int 범위를 구하는 경우, nextInt(int bound)의 결과에 +1을 하면 된다.

 

🌱 Seed

랜덤은 내부에서 씨드(Seed)값을 사용해서 랜덤 값을 구한다. 이 Seed 값이 같으면 실행 결과는 반복 실행해도 같다. new Random() 메서드처럼 생성자를 비워두면 내부에서 System.nanoTime()에 여러가지 복잡한 알고리즘을 섞어서 Seed 값을 생성한다. 따라서 반복 실행해도 결과가 항상 달라진다. 그리고 new Random(int seed)처럼 생성자에 직접 씨드 값을 직접 전달할 수도 있다. 이렇게 Seed 값을 직접 사용하면 결과가 항상 같기 때문에 결과가 달라지는 랜덤값을 구할 수 없다. 하지만 결과가 고정되기 때문에 테스트 코드 같은 곳에서 같은 결과를 검증할 수 있다.

profile
도메인을 이해하는 백엔드 개발자(feat. OOP)

0개의 댓글