[혼공학습단 9기] #5. 3주차 자바 기본 API 클래스, 이건 몰랐을걸?
안녕하세요, 다람쥐 입니다!
한빛미디어 혼공학습단 9기 혼공자바 3주차 진도 중 두 번째 진도를 나갔습니다.
혼공학습단 9기 혼공자바 3주차 진도 중 두 번째 진도는 바로 자바 기본 API 클래스입니다.
java.lang 패키지
java.lang 패키지는 자바 프로그램의 기본적인 클래스를 담고 있는 패키지입니다.
그래서 java.lang 패키지에 있는 클래스와 인터페이스는 import 없이 사용할 수 있는데요.
여태 사용한 java.lang 패키지에 포함된 클래스는 String 과 System 클래스가 있습니다.
java.lang 패키지에 속하는 주요 클래스와 간략한 용도를 먼저 살펴보면 다음과 같습니다.
- Object : 자바 클래스의 최상위 클래스로 사용
- System
- 표준 입력 장치(키보드)에서 데이터를 입력받거나, 표준 출력 장치(모니터)로 출력하기 위해 사용합니다.
- 자바 가상 기계를 종료할 때 사용
- 쓰레기 수집기(GC)를 실행 요청할 때 사용
- Class : 클래스를 메모리로 로딩할 때 사용
- String : 문자열을 저장하고 여러 가지 정보를 얻을 때 사용
- Wrapper Classes ( Byte, Short, Character, Integer, Float, Double, Boolean, Long 등 )
- 기본 타입의 데이터를 갖는 객체를 만들 때 사용
- 문자열을 기본 타입으로 변환할 때 사용
- 입력값 검사에 사용
- Math : 수학 함수를 이용할 때 사용
굉장히 다양한 기본 API 클래스들을 만날 수 있습니다.
그 양이 방대하기에 무작정 외우기 보다는 실습하며 어떤 기능이 있는지 체화하고
나중에 검색이나 API 도큐먼트로 찾아보고 이해할 수 있는 것이 중요합니다.
자바 API 도큐먼트
API 는 Application Programming Interface 의 약자로 편의상 '라이브러리' 라고 불리기도 합니다.
프로그램 개발에 자주 사용되는 클래스 및 인터페이스의 모음을 뜻합니다.
사용했었던 String 과 System 클래스도 모두 API 에 속하는 클래스입니다.
네트워크 환경이 된다면 오라클에서 지원해주는 API 도큐먼트를 참고할 수 있습니다.
링크 : https://docs.oracle.com/en/java/javase/index.html
API 문서를 보고싶은 JDK 버전을 선택합니다. JDK 8 또는 JDK 11 을 선택하여 들어갑니다.
그 후 API Document 메뉴를 클릭합니다.
저는 JDK 11 을 이용하기에 JDK 11 API Document 를 들어갔습니다.
JDK 8 버전은 혼자 공부하는 자바에도 친절하게 설명해주고 있습니다!
JDK 11 API Document 에서 java.base -> java.lang 클래스로 이동합니다.
링크 : https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/package-summary.html
Object 클래스
클래스를 선언할 때 extends 키워드로 다른 클래스를 상속하지 않더라도 암시적으로, 기본적으로, java.lang.Object 를 상속합니다.
Class Object is the root of the class hierarchy. Every class has Object as a superclass. All objects, including arrays, implement the methods of this class.
- Object API 설명 중
Object 는 자바의 최상위 부모 클래스에 해당됩니다.
객체 비교 : equals()
Object 클래스에서 자주 재정의하는 메서드 중 하나로 equals() 메서드가 있습니다.
public boolean equals(Object obj) { ... }
매개 변수 obj 타입은 Object 으로 모든 객체가 매개 변수로 넘겨질 수 있다는 걸 의미합니다.
Object 가 모든 클래스의 최상위 부모 클래스이기에 자동 타입 변환이 가능합니다.
비교 연산자인 == 과 같이 리턴 타입은 boolean ( true, false ) 타입입니다.
두 객체가 동일하면 true, 그렇지 않으면 false 를 반환합니다.
객체 끼리 비교 연산자 == 을 쓴다면 객체의 번지를 조사하는데요.
실제 객체가 가진 값이 같은지를 알고 싶다면 재정의한 equals() 메서드를 사용해야 합니다.
일반적으로 equals() 메서드를 재정의하여 각 클래스에 맞는 비교를 하게 됩니다.
String 클래스의 equals() 메서드 구현
예를 들어 String 클래스의 경우 equals() 메서드로 문자열이 같은지 비교하는 코드로 재정의가 되어 있습니다.
실제 equals() 메서드 코드를 정의 이동하여 봐볼까요?
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
첫 번째로 호출한 객체와 같은 객체를 매개변수로 넣는다면 바로 true 를 반환합니다.
매개 변수로 온 객체가 String 클래스의 인스턴스라면, String 클래스로 타입을 변환시키고
자바에서 최적화되는 문자열의 인코딩 타입에 맞는 클래스의 equals 메서드를 호출시킵니다.
// StringLatin1.java
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}
// StringUTF16.java
static final int HI_BYTE_SHIFT;
static final int LO_BYTE_SHIFT;
static {
if (isBigEndian()) {
HI_BYTE_SHIFT = 8;
LO_BYTE_SHIFT = 0;
} else {
HI_BYTE_SHIFT = 0;
LO_BYTE_SHIFT = 8;
}
}
@HotSpotIntrinsicCandidate
// intrinsic performs no bounds checks
static char getChar(byte[] val, int index) {
assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
index <<= 1;
return (char)(((val[index++] & 0xff) << HI_BYTE_SHIFT) |
((val[index] & 0xff) << LO_BYTE_SHIFT));
}
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
int len = value.length >> 1;
for (int i = 0; i < len; i++) {
if (getChar(value, i) != getChar(other, i)) {
return false;
}
}
return true;
}
return false;
}
기본적으로 자바에서 바이트코드를 UTF16 로 인코딩해줘 문자열을 만드는 방법을 채택하는데,
문자열 압축 (String compaction) 기능을 사용하면 JIT 에서 최적화하는 목적으로 Latin1 인코딩을 사용한다고 하네요.
UTF16 문자열 인코딩 코드를 보면 빅엔디안 환경도 구분하고 조금 더 비트 연산이 필요한 걸 확인할 수 있습니다.
객체 해시코드 ( hashCode() )
객체 해시코드는 객체를 식별하는 하나의 정수값을 의미합니다.
Object 클래스의 hashCode() 메서드는 객체의 메모리 번지를 이용해 해시코드를 만들어줍니다.
컬렉션 프레임워크 HashSet, HashMap, Hashtable 에서 hashCode() 를 사용해 두 객체가 동등한지 비교합니다.
hashCode() 리턴값으로 해시코드 값이 다르면 다른 객체로 판단합니다.
해시코드 값이 같으면 equals() 메서드로 동등 객체인지 다른 객체인지 확인합니다.
만약에 hashCode() 를 사용하지 않으면 어떻게 될까요?
// Key.java
public class Key {
public int number;
public Key(int number) {
this.number = number;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Key) {
Key compareKey = (Key) obj;
if (this.number == compareKey.number) {
return true;
}
}
return false;
}
}
// Example.java
import java.util.HashMap;
public class Example {
public static void main(String[] args) {
HashMap<Key, String> hashMap = new HashMap<Key, String>();
hashMap.put(new Key(1), "홍길동");
String value = hashMap.get(new Key(1));
System.out.println(value); // "null"
}
}
hashMap.get(new Key(1)); 으로 새로운 Key 인스턴스로 조회를 하면은
이전에 해시맵에 저장한 Key(1) 인스턴스가 있음에도 조회가 되지 않습니다!
"null" 으로 출력이 됩니다.
Object 클래스의 hashCode() 메서드의 기본 동작이 어땠었죠~?
객체의 메모리 번지를 반환한다고 했었는데요~
hashMap.get(new Key(1)); 에서 생성한 Key 인스턴스의 메모리 주소와
hashMap.put(new Key(1), "홍길동"); 에서 생성한 Key 인스턴스의 메모리 주소가 다르기 때문에
내부 데이터 (number) 가 같아도 다른 객체로 인식합니다.
다음 코드처럼 hashCode() 메서드를 재정의하여 number 를 반환하는 코드를 추가하고 다시 실행해보겠습니다.
// Key.java
public class Key {
...
@Override
public int hashCode() {
return number;
}
}
// Example.java
import java.util.HashMap;
public class Example {
public static void main(String[] args) {
HashMap<Key, String> hashMap = new HashMap<Key, String>();
hashMap.put(new Key(1), "홍길동");
String value = hashMap.get(new Key(1));
System.out.println(value); // "홍길동"
}
}
Key 객체를 새로 만들어도 내부 데이터 (number) 로 hashCode() 메서드를 반환하기에
해시맵에서 키가 같다고 인식하여 데이터를 찾을 수 있게 됩니다! 👍
객체 문자 정보 ( toString() )
Object 클래스의 toString() 메서드는 객체의 문자 정보를 리턴합니다. 객체의 문자 정보는 객체를 문자열로 표현한 값입니다.
기본적으로 Object 클래스의 toString() 메서드는 '클래스이름@16진수해시코드'로 구성된 문자 정보를 리턴합니다.
// Object.java
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
클래스 메서드명과 hashCode() 메서드를 16진수 문자열로 만들어서 반환합니다.
Object 하위 클래스는 toString() 메서드를 재정의하여 간결하고 유익한 정보를 반환해주도록 합니다.
예를 들어 java.util 패키지의 Date 클래스는 toString() 메서드를 재정의하여 현재 시스템의 날짜와 시간 정보를 리턴합니다.
// Example.java
import java.util.Date;
public class Example {
public static void main(String[] args) {
Object obj1 = new Object();
Date obj2 = new Date();
System.out.println(obj1.toString()); // java.lang.Object@1c4af82c
System.out.println(obj2.toString()); // Tue Jan 17 15:14:26 KST 2023
}
}
// Date.java
* Converts this {@code Date} object to a {@code String}
* of the form:
* <blockquote><pre>
* dow mon dd hh:mm:ss zzz yyyy</pre></blockquote>
* where:<ul>
* <li>{@code dow} is the day of the week ({@code Sun, Mon, Tue, Wed,
* Thu, Fri, Sat}).
* <li>{@code mon} is the month ({@code Jan, Feb, Mar, Apr, May, Jun,
* Jul, Aug, Sep, Oct, Nov, Dec}).
* <li>{@code dd} is the day of the month ({@code 01} through
* {@code 31}), as two decimal digits.
* <li>{@code hh} is the hour of the day ({@code 00} through
* {@code 23}), as two decimal digits.
* <li>{@code mm} is the minute within the hour ({@code 00} through
* {@code 59}), as two decimal digits.
* <li>{@code ss} is the second within the minute ({@code 00} through
* {@code 61}, as two decimal digits.
* <li>{@code zzz} is the time zone (and may reflect daylight saving
* time). Standard time zone abbreviations include those
* recognized by the method {@code parse}. If time zone
* information is not available, then {@code zzz} is empty -
* that is, it consists of no characters at all.
* <li>{@code yyyy} is the year, as four decimal digits.
* </ul>
*
* @return a string representation of this date.
* @see java.util.Date#toLocaleString()
* @see java.util.Date#toGMTString()
*/
public String toString() {
// "EEE MMM dd HH:mm:ss zzz yyyy";
BaseCalendar.Date date = normalize();
StringBuilder sb = new StringBuilder(28);
int index = date.getDayOfWeek();
if (index == BaseCalendar.SUNDAY) {
index = 8;
}
convertToAbbr(sb, wtb[index]).append(' '); // EEE
convertToAbbr(sb, wtb[date.getMonth() - 1 + 2 + 7]).append(' '); // MMM
CalendarUtils.sprintf0d(sb, date.getDayOfMonth(), 2).append(' '); // dd
CalendarUtils.sprintf0d(sb, date.getHours(), 2).append(':'); // HH
CalendarUtils.sprintf0d(sb, date.getMinutes(), 2).append(':'); // mm
CalendarUtils.sprintf0d(sb, date.getSeconds(), 2).append(' '); // ss
TimeZone zi = date.getZone();
if (zi != null) {
sb.append(zi.getDisplayName(date.isDaylightTime(), TimeZone.SHORT, Locale.US)); // zzz
} else {
sb.append("GMT");
}
sb.append(' ').append(date.getYear()); // yyyy
return sb.toString();
}
private static final StringBuilder convertToAbbr(StringBuilder sb, String name) {
sb.append(Character.toUpperCase(name.charAt(0)));
sb.append(name.charAt(1)).append(name.charAt(2));
return sb;
}
직접 클래스를 만들 때 toString() 메서드로 유의미한 정보를 넘겨줄 수 있습니다.
// SmartPhone.java
public class SmartPhone {
private String company;
private String os;
public SmartPhone(String company, String os) {
this.company = company;
this.os = os;
}
@Override
public String toString() {
return "SmartPhone{" +
"company='" + company + '\'' +
", os='" + os + '\'' +
'}';
}
}
// Example.java
public class Example {
public static void main(String[] args) {
SmartPhone myPhone = new SmartPhone("구글", "안드로이드");
String strObj = myPhone.toString();
System.out.println(strObj); // SmartPhone{company='구글', os='안드로이드'}
System.out.println(myPhone); // SmartPhone{company='구글', os='안드로이드'}
}
}
// PrintStream.java
public void println(Object x) {
String s = String.valueOf(x);
synchronized (this) {
print(s);
newLine();
}
}
// String.java
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
System.out.println() 메서드 중 객체를 매개 변수로 넣을 경우 String.valueOf() 를 호출합니다.
valueOf() 메서드는 매개 변수로 들어온 객체가 null 이면 "null" 문자열을 보내고, 아니면 toString() 메서드를 보냅니다.
System 클래스
자바 프로그램은 운영체제에서 바로 실행되는 것이 아니라 JVM 위에서 실행됩니다.
운영체제의 모든 기능을 직접 이용하기는 어려운데요!
java.lang 패키지의 System 클래스를 이용하면 운영체제의 일부 기능을 이용할 수 있습니다.
프로그램 종료, 키보드 입력, 모니터 출력, 현재 시간 읽기 등의 API 를 제공합니다.
System 클래스의 모든 필드와 메서드는 정적 필드와 정적 메서드로 구성되어 있습니다.
프로그램 종료 ( exit() )
강제적으로 JVM을 종료시키려고할 때 System 클래스의 exit() 메서드를 호출합니다.
현재 실행하고 있는 프로세스를 강제로 종료시키는 역할을 합니다.
int 타입의 매개 변수가 있는데, 종료 상태값을 넘겨줄 수 있습니다.
일반적으로 정상 종료일 경우 0 으로 줍니다.
// Example.java
public class Example {
public static void main(String[] args) {
for (int i = 0; i < 10; ++i) {
if (i == 5) {
System.exit(0);
// break;
}
}
System.out.println("마무리 코드"); // 실행되지 않음
}
}
현재 시각 읽기
System 클래스의 currentTimeMillis() 메소드와 nanoTime() 메소드는 컴퓨터의 시계에서 현재 시간을 읽어서
밀리 세컨드 (1/1000초) 단위와 나노 세컨드 (1/10^9초) 단위의 long 값을 리턴합니다.
public class Example {
public static void main(String[] args) {
long time = System.currentTimeMillis(); // time: 1673945406580
long time2 = System.nanoTime(); // time2 : 81492169069500
}
}
public class Example {
public static void main(String[] args) {
long time1 = System.nanoTime();
int sum = 0;
for (int i = 1; i <= 1_000_000; i++) {
sum += i;
}
long time2 = System.nanoTime();
System.out.println("1 ~ 1백만 까지의 합 : " + sum);
// 계산에 3991459 나노초가 소모되었습니다.
System.out.println("계산에 " + (time2 - time1) + " 나노초가 소모되었습니다.");
double seconds = (double) (time2 - time1) / 1_000_000_000;
// 계산에 0.003991459 초가 소모되었습니다.
System.out.println("계산에 " + (seconds) + " 초가 소모되었습니다.");
}
}
Class 클래스
자바는 클래스와 인터페이스의 메타 데이터를 java.lang 패키지에 소속된 Class 클래스로 관리합니다.
메타 데이터는 클래스의 이름, 생성자 정보, 필드 정보, 메소드 정보를 말합니다.
Class 객체 얻기 ( getClass(), forName() )
프로그램에서 Class 객체를 얻기 위해서는 다음 세 가지 방법 중 하나를 이용합니다.
클래스에서 얻는 방법
객체 없이 클래스 이름만 가지로 Class 객체를 얻는 방법입니다.
public class Example {
public static void main(String[] args) {
Class clazz = Example.class;
System.out.println(clazz); // class Example
try {
Class clazz2 = Class.forName("Example");
System.out.println(clazz2); // class Example
} catch (ClassNotFoundException cnfe) {
cnfe.printStackTrace();
}
}
}
객체에서 얻는 방법
클래스에서 객체가 이미 생성되었을 때 사용할 수 있습니다.
예를 들어 String 클래스의 Class 객체는 아래와 같이 얻을 수 있습니다.
public class Example {
public static void main(String[] args) {
Class clazz = "다람쥐".getClass();
System.out.println(clazz); // class java.lang.String
}
}
클래스 객체에서 클래스의 전체 이름과 간단한 이름, 패키지 이름을 얻을 수 있습니다.
public class Example {
public static void main(String[] args) {
Class clazz = "다람쥐".getClass();
System.out.println(clazz.getName()); // java.lang.String
System.out.println(clazz.getSimpleName()); // String
System.out.println(clazz.getPackage().getName()); // java.lang
}
}
클래스 경로를 활용해서 리소스 절대 경로 얻기
Class 객체는 해당 클래스의 파일 경로 정보를 가지고 있습니다.
이 경로로 다른 리소스 파일 (이미지, XML, Property 파일 등)의 경로를 얻을 수 있습니다.
UI 프로그램에서 주로 활용됩니다.
Car 클래스가 위치하는 경로에 photo.jpg 파일이 있다고 가정해보겠습니다.
class Car {
}
public class Example {
public static void main(String[] args) {
Class clazz = Car.class;
String photo1Path = clazz.getResource("photo1.jpg").getPath();
String photo2Path = clazz.getResource("images/photo2.jpg").getPath();
}
}
String 클래스
문자열을 생성하는 방법과 추출, 비교, 찾기, 분리, 변환 등을 제공하는 메소드를 제공합니다.
문자열은 데이터로 많이 사용하기에 위 메소드를 많이 씁니다.
String 생성자
자바의 문자열은 java.lang 패키지의 String 클래스의 인스턴스로 관리됩니다.
문자열 리터럴 ( "문자열" ) 은 String 객체로 자동으로 생성됩니다.
String 클래스의 다양한 생성자를 이용해서 직접 String 객체를 생성할 수 있습니다.
byte[] 바이트 배열로 문자열을 만들어주는 생성자를 활용할 수 있습니다.
파일의 내용을 읽거나 네트워크로 받은 데이터는 보통 byte[] 배열이므로 자주 사용하는 생성자입니다.
public class Example {
public static void main(String[] args) {
byte[] bytes = { 72, 101, 108, 108, 111, 32, 74, 97, 118, 97 };
String str1 = new String(bytes);
System.out.println(str1); // Hello Java
String str2 = new String(bytes, 6, 4);
System.out.println(str2); // Java
}
}
byte[] 배열을 받는 생성자 중에 오프셋(offset) 과 길이(length) 를 받는 생성자가 있습니다.
6 번째 ( 74 ) 부터 네 개의 길이를 읽게 됩니다.
다음 예제는 키보드에서 읽은 바이트 배열을 문자열로 반환합니다.
System.in.read() 메서드는 키보드에서 입력한 내용을 매개 변수로 넘어온 byte 배열에 저장합니다.
그리고 읽은 바이트 수를 반환합니다.
import java.io.IOException;
public class Example {
public static void main(String[] args) throws IOException {
byte[] bytes = new byte[100];
System.out.print("입력: ");
int readByteNo = System.in.read(bytes); // Hello\n
String str = new String(bytes, 0, readByteNo - 1); // Hello
System.out.println(str);
}
}
Windows 환경에선 \r ( 13 ) , \n ( 10 ) 까지 나타나지만, macOS 환경에선 \n ( 10 ) 까지 뒤에 붙여집니다.
혼공자바 예제에선 readByteNo - 2 으로 길이를 구했지만, macOS 환경에선 readByteNo - 1 으로 길이를 구했습니다.
String 메소드
String 클래스에서 사용 빈도수가 높은 메소드를 정리했습니다.
리턴 타입 | 메소드 이름(매개 변수) | 설명 |
char | charAt(int index) | 특정 위치의 문자를 리턴합니다. |
boolean | equals(Object anObject) | 두 문자열을 비교합니다. |
byte[] | getBytes() | byte[]로 리턴합니다. |
byte[] | getBytes(Charset charset) | 주어진 문자셋으로 인코딩한 byte[] 으로 리턴합니다. |
int | indexOf(String str) | 문자열 내에서 주어진 문자열의 위치를 리턴합니다. |
int | length() | 총 문자의 수를 리턴합니다. |
String | replace(CharSequence target, CharSequence replacement) | target 부분을 replacement 로 대치한 새로운 문자열을 리턴합니다. |
String | substring(int beginIndex) | beginIndex 위치에서 끝까지 잘라낸 새로운 문자열을 리턴합니다. |
String | substring(int beginIndex, int endIndex) | beginIndex 위치에서 endIndex 위치 전까지 잘라낸 새로운 문자열을 리턴합니다. |
String | toLowerCase() | 알파벳 소문자로 변환한 새로운 문자열을 리턴합니다. |
String | toUpperCase() | 알파벳 대문자로 변환한 새로운 문자열을 리턴합니다. |
String | trim() | 앞뒤 공백을 제거한 새로운 문자열을 리턴합니다. |
String | valueOf(int i) valueOf(double d) |
기본 타입 값을 문자열로 리턴합니다. |
import java.io.IOException;
import java.io.UnsupportedEncodingException;
public class Example {
public static void main(String[] args) throws IOException {
// 문자 추출
String subject = "자바 프로그래밍";
char charValue = subject.charAt(3); // '프'
System.out.println(charValue);
String ssn = "010624-3230123";
char sexFromSsn = ssn.charAt(7);
switch (sexFromSsn) {
case '1':
case '3':
System.out.println("남자입니다."); // call
break;
case '2':
case '4':
System.out.println("여자입니다.");
break;
}
// 문자열 비교
String strVar1 = new String("신민철");
String strVar2 = "신민철";
String strVar3 = "신민철";
System.out.println(strVar1 == strVar2); // false
System.out.println(strVar2 == strVar3); // true
System.out.println(strVar1.equals(strVar2)); // true
System.out.println(strVar2.equals(strVar3)); // true
// 바이트 배열로 변환
try {
byte[] bytes1 = "다람쥐".getBytes("EUC-KR");
byte[] bytes2 = "다람쥐".getBytes("UTF-8");
System.out.println(new String(bytes1, "EUC-KR")); // 다람쥐
System.out.println(new String(bytes2, "UTF-8")); // 다람쥐
} catch (UnsupportedEncodingException usee) {
usee.printStackTrace();
}
// 문자열 찾기
String subjectName = "자바 프로그래밍";
int location = subjectName.indexOf("프로그래밍"); // 3
System.out.println(location);
if (subject.indexOf("자바") != -1) {
System.out.println("자바와 관련된 책이군요."); // call
} else {
System.out.println("자바와 관련없는 책이군요.");
}
// 문자열 길이
String ssnWithoutDelimiter = "7306241234567";
int ssnLength = ssnWithoutDelimiter.length();
final int VALID_SSN_LENGTH = 13;
if (ssnLength == VALID_SSN_LENGTH) {
System.out.println("주민번호 자리 수가 맞습니다."); // call
} else {
System.out.println("주민번호 자리 수가 틀립니다.");
}
// 문자열 대치하기
String oldStr = "자바는 객체 지향 언어입니다. 자바는 풍부한 API를 지원합니다.";
String newStr = oldStr.replace("자바", "JAVA");
System.out.println(oldStr); // "자바는 객체 지향 언어입니다. 자바는 풍부한 API를 지원합니다."
System.out.println(newStr); // "JAVA는 객체 지향 언어입니다. JAVA는 풍부한 API를 지원합니다."
// 문자열 잘라내기
String ssn88 = "880815-1234567";
String firstPartFromSsn88 = ssn88.substring(0, 6); // "880815"
String lastPartFromSsn88 = ssn88.substring(7); // "1234567"
System.out.println(firstPartFromSsn88);
System.out.println(lastPartFromSsn88);
// 알파벳 소문자 / 대문자 변경
String originalStr = "Chipmunk";
String lowerCaseStr = originalStr.toLowerCase(); // "chipmunk"
String upperCaseStr = originalStr.toUpperCase(); // "CHIPMUNK"
System.out.println(lowerCaseStr);
System.out.println(upperCaseStr);
// 문자열 앞뒤 공백 잘라내기 ( trim() )
String strWithWhitespaces = " Chipmunk ";
String trimmedStr = strWithWhitespaces.trim(); // "Chipmunk"
System.out.println(trimmedStr);
// 문자열 변환
System.out.println(String.valueOf(10)); // "10"
System.out.println(String.valueOf(10.5)); // "10.5"
System.out.println(String.valueOf(true)); // "true"
}
}
Wrapper(포장) 클래스
자바는 기본 타입 (byte, char, short, int, long, float, double, boolean) 의 값을 갖는 객체를 생성할 수 있습니다.
이런 객체를 랩퍼(Wrapper, 포장) 클래스라고 합니다.
랩퍼 객체의 특징은 기본 타입 값은 외부에서 변경할 수 없다는 점입니다.
내부의 값을 변경하고 싶다면 새로운 랩퍼 객체를 만들어야 합니다.
이를 불변 객체라고도 합니다.
랩퍼 객체는 컬렉션 프레임워크에서 기본 타입 값을 객체로 생성해서 관리해야할 때 사용됩니다.
java.lang 패키지에 포함되는데 기본 타입에 대응되는 클래스들이 있습니다.
char 타입과 int 타입이 각각 Character, Integer 으로 변경되고, 기본 타입의 첫 문자를 대문자로 바꾼 이름을 가지고 있습니다.
기본 타입 | 랩퍼 클래스 |
byte | Byte |
char | Character |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
boolean | Boolean |
박싱 (Boxing) 과 언박싱 (Unboxing)
박싱 : 기본 타입의 값 -> 랩퍼 객체로 변환
언박싱 : 랩퍼 객체 -> 기본 타입의 값으로 변환
기본 값을 객체로 만드는 걸 박싱이라고 하며, 한 번 더 감싼다는 의미로 이해하면 편합니다.
반대로 언박싱은 상자를 개봉한다는 의미로, 객체에서 기본 타입의 값을 꺼내온다고 이해하면 편합니다.
랩퍼 클래스 생성자를 만들 때 기본 타입 또는 문자열로 넘겨줘서 생성합니다. ( Character 의 경우 문자열 생성자가 없음 )
Short obj = new Short("100");
생성자를 이용하지 않아도 각 랩퍼 클래스마다 가지고 있는 valueOf() 메서드를 사용할 수도 있다.
public class Example {
public static void main(String[] args) {
// 박싱
Integer obj1 = new Integer(100);
Integer obj2 = new Integer("200");
Integer obj3 = new Integer("300");
// 언박싱
int value1 = obj1.intValue(); // 100
int value2 = obj2.intValue(); // 200
int value3 = obj3.intValue(); // 300
System.out.println(value1);
System.out.println(value2);
System.out.println(value3);
}
}
자동 박싱과 언박싱
기본 타입 값을 직접 박싱, 언박싱하지 않아도 자동적으로 박싱과 언박싱이 일어날 수 있습니다.
자동 박싱은 랩퍼 클래스 타입에 기본값이 대입될 경우에 발생합니다.
Integer obj = 100; // 자동 박싱
자동 언박싱은 기본 타입에 포장 객체가 대입되는 경우와 연산에서 발생합니다.
Integer obj = new Integer(200);
int value1 = obj; // 자동 언박싱
int value2 = obj + 100; // 자동 언박싱
문자열을 기본 타입 값으로 변환
랩퍼 클래스를 이용해 문자열을 기본 타입 값으로 변환할 때에도 많이 사용됩니다.
대부분 랩퍼 클래스엔 'parse + 기본 타입 이름' 으로 되어 있는 정적 메서드를 제공합니다.
public class Example {
public static void main(String[] args) {
int value1 = Integer.parseInt("10"); // 10
double value2 = Double.parseDouble("3.14"); // 3.14
boolean value3 = Boolean.parseBoolean("true"); // true
System.out.println(value1);
System.out.println(value2);
System.out.println(value3);
}
}
포장 값 비교
랩퍼 객체로 내부 값을 비교할 때 == 연산자와 != 연산자를 사용하지 않는 것이 좋습니다.
이는 내부 값 비교가 아니라 객체 메모리 번지 비교하기 때문인데요.
public class Example {
public static void main(String[] args) {
Integer obj1 = 300;
Integer obj2 = 300;
System.out.println(obj1 == obj2); // false
}
}
그러나 내부 값의 범위에 따라 동작이 달라지는 게 있는데요.
박싱된 값이 범위 안에 있다면 == 와 != 연산자로 내부의 값을 바로 비교할 수 있습니다.
타입 | 값의 범위 |
boolean | true, false |
char | \u0000 ~ \u007f |
byte, short, int | -128 ~ 127 |
public class Example {
public static void main(String[] args) {
System.out.println("[-128~127 초과값일 경우]");
Integer obj1 = 300;
Integer obj2 = 300;
System.out.println(obj1 == obj2); // false
System.out.println(obj1.equals(obj2)); // true
System.out.println(obj1.intValue() == obj2.intValue()); // true
System.out.println("[-128~127 범위값일 경우]");
Integer obj3 = 100;
Integer obj4 = 100;
System.out.println(obj3 == obj4); // true
System.out.println(obj3.equals(obj4)); // true
System.out.println(obj3.intValue() == obj4.intValue()); // true
}
}
어떻게 -128 ~ 127 범위값일 때 객체 비교가 되는 걸까?
그렇다면 어떻게 -128 ~ 127 범위값일 때 동등비교가 True 로 되는 걸까?
동등비교가 True 라는 건 같은 메모리 번지를 쓸 때 True 가 됩니다.
자바 API 에서 어떻게 범위값 안에 있으면 같은 메모리 번지를 생성할 수 있는지 알아보도록 할까요.
인텔리제이 IDE 에서 브레이크포인트를 걸어보았습니다.
자동 박싱이 발생하는 코드에 브레이크 포인트를 걸었습니다.
Force Step Into 으로 들어가자 Integer 클래스의 valueOf 메서드를 호출하는 것을 볼 수 있습니다!
API 문서 주석 설명에도 써있듯이 퍼포먼스 증가를 위해 특정 범위 ( -128 ~ 127, inclusive ) 를 캐싱한다고 나와있습니다.
캐싱이란, 메모리 같이 어딘가에 빠르게 접근 가능한 곳에 저장한 채로 들고있다가 필요시에 적은 비용으로 빠르게 가져올 수 있도록 해주는 걸 의미합니다.
valueOf 매개변수로 들어온 i 의 범위 값이 IntegetCache.low 와 IntegerCache.high 값 사이라면
IntegerCache.cache[] 에서 정적으로 정의된 Integer 객체를 불러옵니다.
IntegerCache.cache 를 따라가다 보면 설정된 범위에 new Integer() 으로 미리 생성한 걸 확인할 수 있습니다.
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer[] cache;
static Integer[] archivedCache;
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
// Load IntegerCache.archivedCache from archive, if possible
VM.initializeFromArchive(IntegerCache.class);
int size = (high - low) + 1;
// Use the archived cache if it exists and is large enough
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
for(int k = 0; k < c.length; k++)
c[k] = new Integer(j++);
archivedCache = c;
}
cache = archivedCache;
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
IntegerCache 의 high 값은 static { } 블록으로 기본 값을 127 로 하거나 JVM 옵션 값으로 불러옵니다.
프로그램을 실행할 때 -XX:AutoBoxCacheMax 옵션으로 IntegerCache 의 high 값을 설정해줄 수 있습니다.
$ java -XX:AutoBoxCacheMax=500 Example
[-128~127 초과값일 경우]
true
true
true
[-128~127 범위값일 경우]
true
true
true
high 값을 500 으로 설정해서 방금 전 자바 프로그램을 실행하면
300 을 자동 박싱으로 생성한 Integer 객체의 동등 비교가 true 로 동일한 것을 확인할 수 있습니다!
-128 ~ 500 까지 미리 Intger[] IntegerCache.cache 에 생성하여 valueOf 메서드에서
미리 생성하여 캐싱된 Integer 객체를 반환한 것을 확인할 수 있습니다.
Integer 객체 내에서 hashCode() 메서드와 toString() 메서드가 재정의되어 있어 메모리 번지에 따른 해시 코드를 구할 수 없는데요.
System.identityHashCode() 메소드로 객체의 고유한 해시 코드를 구할 수 있습니다.
위 코드에 이를 추가하여 정말로 같은 메모리 번지를 갖고있는지 확인해봅시다.
public class Example {
public static void main(String[] args) {
System.out.println("[-128~127 초과값일 경우]");
Integer obj1 = 300;
Integer obj2 = 300;
System.out.println(obj1 == obj2); // false
System.out.println(obj1.equals(obj2)); // true
System.out.println(obj1.intValue() == obj2.intValue()); // true
System.out.println(Integer.toHexString(System.identityHashCode(obj1)));
System.out.println(Integer.toHexString(System.identityHashCode(obj2)));
System.out.println("[-128~127 범위값일 경우]");
Integer obj3 = 100;
Integer obj4 = 100;
System.out.println(obj3 == obj4); // true
System.out.println(obj3.equals(obj4)); // true
System.out.println(obj3.intValue() == obj4.intValue()); // true
System.out.println(Integer.toHexString(System.identityHashCode(obj3)));
System.out.println(Integer.toHexString(System.identityHashCode(obj4)));
}
}
[-128~127 초과값일 경우]
false
true
true
1c4af82c
379619aa
[-128~127 범위값일 경우]
true
true
true
cac736f
cac736f
IntegerCache 클래스의 high 값이 기본값(128) 이라면 300 값은 메모리 번지가 다르고, -128 ~ 127 범위 값인 경우 같은 걸 확인할 수 있습니다.
다시 한 번 high 값을 늘려 300 값도 범위안에 포함시킨다면 어떻게 될까요?
$ java -XX:AutoBoxCacheMax=500 Example
[-128~127 초과값일 경우]
true
true
true
1c4af82c
1c4af82c
[-128~127 범위값일 경우]
true
true
true
379619aa
379619aa
300 값을 자동 박싱한 Interger 객체의 메모리 번지가 같은 걸 확인할 수 있습니다.
Math 클래스로 랜덤한 수 얻기
java.lang.Math 클래스는 수학 계산에 사용할 수 있는 메소드를 제공합니다.
모두 정적 메소드이므로 Math 클래스로 바로 사용할 수 있습니다.
- abs 메소드는 절대값을 반환합니다. 정수와 실수 모두 사용 가능합니다.
- ceil 메소드는 올림값을 반환합니다. 실수를 받아 올림해줍니다.
- floor 메소드는 버림값을 반환합니다. 실수를 받아 내림해줍니다.
- max 메소드는 두 값 중 최대값을 반환합니다. 정수와 실수 모두 사용 가능합니다.
- min 메소드는 두 값 중 최소값을 반환합니다. 정수와 실수 모두 사용 가능합니다.
- random 메소드는 0.0 <= x < 1.0 난수값을 반환합니다.
- rint 메소드는 가까운 정수의 실수값을 반환합니다.
- round 메소드는 반올림한 값을 알려줍니다. 실수값을 매개 변수로 받아 long 정수형으로 반환합니다.
모두 다 유용하게 쓰이는 메소드입니다.
이번엔 random 메소드를 활용하여 1 ~ 3 의 값을 랜덤으로 얻어볼까요?
0.0 <= Math.random() < 1.0 에서 어떻게 1부터 3의 값을 랜덤으로 얻을 수 있을까요?
최대 범위를 높이기 위해 각 양 변에 3을 곱해봅니다!
0.0 * 3 <= Math.random() * 3 < 3.0
0.0 <= Math.random() * 3 < 3.0
으로 만들 수 있습니다!
최소 값이 1이므로 각 양변에 1을 더하면 범위가 아래처럼 됩니다.
1.0 <= Math.random() * 3 + 1 < 4.0
int 정수형으로 타입변환하면 소수점을 모두 버려 정수형으로 됩니다.
(int) 1.0 <= (int) (Math.random() * 3) + 1 < (int) (4.0)
1 <= (int) (Math.random() * 3) + 1 <= 3
으로 1부터 3의 난수값을 얻을 수 있습니다.
실행할 때 마다 1 ~ 3의 값이 랜덤으로 내려갑니다!
public class Example {
public static void main(String[] args) {
int randomNumber = (int) (Math.random() * 3) + 1;
System.out.println(randomNumber); // 1, 2, 3
}
}
해당 코드를 실행할 때 마다 달라지므로 무작위성으로 재밌는 게임을 만들 수 있습니다.
1 ~ 3 값을 랜덤으로 만들어 프로그램과 가위, 바위, 보를 하는 게임을 만들어 볼까요?
import java.util.Scanner;
public class Example {
enum RockScissorPaper {
ROCK(1),
SCISSOR(2),
PAPER(3);
private final int value;
RockScissorPaper(int value) { this.value = value; };
public int getValue() { return this.value; }
public static RockScissorPaper findByValue(int value){
for(RockScissorPaper v : values()){
if(v.getValue() == (value)){
return v;
}
}
return null;
}
public boolean isDraw(RockScissorPaper another) {
if (this != another) {
return false;
}
return true;
}
public boolean isWin(RockScissorPaper another) {
if (this == ROCK && another == SCISSOR) {
return true;
}
if (this == SCISSOR && another == PAPER) {
return true;
}
if (this == PAPER && another == ROCK) {
return true;
}
return false;
}
public boolean isLose(RockScissorPaper another) {
return !another.isDraw(this) && another.isWin(this);
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("Input ( 1: 바위, 2: 가위, 3: 보 ): ");
int userInput = scanner.nextInt();
RockScissorPaper userRCPValue = RockScissorPaper.findByValue(userInput);
if (userRCPValue == null) {
System.out.println("유효한 값이 아닙니다.");
return;
}
int computerRandomValue = (int) (Math.random() * 3) + 1;
RockScissorPaper computerRCPValue = RockScissorPaper.findByValue(computerRandomValue);
assert computerRCPValue != null;
if (userRCPValue.isWin(computerRCPValue)) {
System.out.println("이겼습니다! ( 나: " + userRCPValue + ", 컴퓨터: " + computerRCPValue + " )");
}
else if (userRCPValue.isLose(computerRCPValue)) {
System.out.println("졌습니다. ㅠㅠ ( 나: " + userRCPValue + ", 컴퓨터: " + computerRCPValue + " )");
}
else if (userRCPValue.isDraw(computerRCPValue)) {
System.out.println("비겼습니다. ( 나: " + userRCPValue + ", 컴퓨터: " + computerRCPValue + " )");
}
}
}
컴퓨터는 1 부터 3의 난수 값으로 바위, 가위, 보를 선택합니다!
프로그램이 실행할 때 마다 컴퓨터의 값이 바뀌게 됩니다.
무작위성을 추가한 것 만으로 재미있는 프로그램을 만들 수 있게 됩니다.
java.util 패키지
자바 표준 API 에는 날짜 정보와 관련된 클래스들이 있습니다.
대표적으로 Date, Calendar 클래스가 있습니다.
Date 클래스는 날짜와 시간 정보를 저장하는 클래스입니다.
Date 객체는 특정 시점의 날짜를 표현합니다. 그 안에는 특정 시점의 연도, 월, 일, 시간 정보가 있습니다.
Calendar 클래스는 운영체제의 날짜와 시간을 얻을 때 사용 합니다.
달력을 표현한 클래스입니다. 해당 운영체제의 Calendar 객체를 얻으면, 연도, 월, 일, 요일, 오전/오후, 시간 등의 정보를 얻을 수 잇습니다.
Date 클래스
Date 객체는 다음과 같이 생성할 수 있습니다.
Date now = new Date();
Date 객체의 toString() 메서드는 영문으로 날짜를 리턴합니다.
원하는 날짜 형식의 문자열을 얻고 싶다면 java.text 패키지의 SimpleDateFormat 클래스와 함께 사용합니다.
SimpleDateFormat sdf = new SimpleDateFormat("yyyy년 MM월 dd일 hh시 mm분 ss초");
형식 문자열로 원하는 포맷을 만들 수 있습니다.
자세한 형식 문자열은 SimpleDateFormat API 문서에서 확인할 수 있습니다.
링크 : https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/text/SimpleDateFormat.html
String strNow = sdf.format(now);
format 메소드에 Date 객체를 넣어 포맷팅한 문자열을 얻을 수 있습니다.
import java.text.SimpleDateFormat;
import java.util.Date;
public class Example {
public static void main(String[] args) {
Date now = new Date();
String strNow1 = now.toString(); // Tue Jan 17 21:24:11 KST 2023
System.out.println(strNow1);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy년 MM월 dd일 hh시 mm분 ss초");
String strNow2 = sdf.format(now); // 2023년 01월 17일 09시 24분 11초
System.out.println(strNow2);
}
}
Calendar 클래스
Calendar 클래스는 달력을 표현한 클래스입니다.
Calendar 클래스 자체는 추상 클래스 입니다.
따라서 new 연산자로 인스턴스를 생성할 수 없습니다.
정적 메소드인 getInstance() 메소드를 이용해 현재 운영체제에 설정된 시간대(Timezone)를 기준으로 한 Calendar 하위 객체를 얻을 수 있습니다.
Calendar 객체의 get() 메소드를 사용하여 날짜와 시간에 대한 정보를 얻을 수 있습니다.
get() 메소드를 호출할 때 사용한 매개값은 모두 Calendar 클래스에 선언되어 있는 상수들입니다.
import java.util.Calendar;
public class Example {
public static void main(String[] args) {
Calendar now = Calendar.getInstance();
int year = now.get(Calendar.YEAR);
int month = now.get(Calendar.MONTH) + 1;
int day = now.get(Calendar.DAY_OF_MONTH);
int week = now.get(Calendar.DAY_OF_WEEK);
String strWeek = null;
switch (week) {
case Calendar.MONDAY:
strWeek = "월";
break;
case Calendar.TUESDAY:
strWeek = "화";
break;
case Calendar.WEDNESDAY:
strWeek = "수";
break;
case Calendar.THURSDAY:
strWeek = "목";
break;
case Calendar.FRIDAY:
strWeek = "금";
break;
case Calendar.SATURDAY:
strWeek = "토";
break;
default:
strWeek = "일";
}
int amPm = now.get(Calendar.AM_PM);
String strAmPm = null;
if (amPm == Calendar.AM) {
strAmPm = "오전";
} else {
strAmPm = "오후";
}
int hour = now.get(Calendar.HOUR);
int minute = now.get(Calendar.MINUTE);
int second = now.get(Calendar.SECOND);
System.out.print(year + "년 ");
System.out.print(month + "월 ");
System.out.print(day + "일 ");
System.out.print(strWeek + "요일 ");
System.out.print(strAmPm + " ");
System.out.print(hour + "시 ");
System.out.print(minute + "분 ");
System.out.print(second + "초 ");
// 2023년 1월 17일 화요일 오후 9시 29분 55초
}
}
Calendar.MONTH 으로 값을 가져오면, 1월부터 0 으로 알려주기 때문에 + 1 을 해줍니다.
WEEK, AM_PM 의 경우 정확한 값의 Calendar 상수를 반환합니다. 따라서 추가로 어떤 값인지 확인을 더 해야 했습니다.
512 페이지 문제 8번 풀이하기
512 페이지의 8번 문제는 앞서 설명한 Integer 객체의 IntegerCache 범위값에 따른 캐싱된 객체를 사용하는 케이스의 문제입니다.
박싱된 Integer 객체를 == 연산자로 비교할 때, 100을 박싱한 Integer 객체는 동등비교가 true 가 나오지만
300을 박싱한 Integer 객체는 false 가 나오는 이유를 설명해주세요.
public class Example {
public static void main(String[] args) {
Integer obj1 = 100;
Integer obj2 = 100;
Integer obj3 = 300;
Integer obj4 = 300;
System.out.println(obj1 == obj2); // true
System.out.println(obj3 == obj4); // false
}
}
Integer 객체에서 자동박싱할 때 valueOf() 메소드를 호출합니다.
valueOf() 메소드로 전달한 기본 값이 IntegerCache 의 범위 안에 속하는지 확인합니다.
기본 범위는 -128 ~ 127 이므로 100 값은 해당됩니다.
클래스를 로드할 때 캐싱 용도로 생성한 Integer 객체의 배열 중의 하나를 바로 반환합니다.
따라서 예전에 생성한 같은 객체를 참조하므로 동등 비교시 true 가 나옵니다.
300 값을 자동박싱한 경우 IntegerCache 범위 값에 속하지 않으므로 새로운 Integer 객체를 생성하게 됩니다.
따라서 서로 다른 객체를 참조하므로 동등 비교시 false 가 나옵니다.
만약 -XX:AutoBoxCacheMax 옵션으로 -XX:AutoBoxCacheMax=300 같이 300 이상의 값을 IntegerCache 의 high 범위를 설정해준다면 -128 ~ 300 까지 Integer 객체를 캐싱합니다.
이 경우에 동등 비교시 true 가 나오게 됩니다.
마무리
오늘까지 혼공학습단 9기 혼공자바 Chapter 11 까지 공부를 마쳤습니다!
Integer 클래스에서 사용하는 IntegerCache 에 관한 로직이 흥미로웠습니다.
성능을 올려주는 일종의 흑마법(?)을 사용자가 눈치챌 수 없게 숨겨져 있었는데요.
흑마법이지만 매 번 새로운 객체를 생성해서 발생하는 성능 저하 문제가 더 크기 때문에 로직을 추가할 수 밖에 없는 거였구나를 느낄 수 있었네요.
혼공자바 공부하는 것 넘넘 재밌네요.
다음 포스팅에서 뵐게요~
혼공학습단 9기 다른 글 보러가기
2023.01.17 - [자유/대외 활동] - [혼공학습단 9기] #4. 3주차 예외, 200% 이해하기
2023.01.10 - [자유/대외 활동] - [혼공학습단 9기] #3. 2주차 객체 지향 프로그래밍을 왜 쓸까?
2023.01.08 - [자유/대외 활동] - [혼공학습단 9기] #2. 자바 기본 정말 안다고 생각해?
2023.01.08 - [자유/대외 활동] - [혼공학습단 #1] 한빛미디어 혼공학습단 9기 선정