2024. 1. 22. 00:45ㆍ공부 중/Java
다형성에 대하여 정리하기 전에 접근제한자를 활용하는 거 조금 공부함.
1. 접근제한자 활용
가. Encapsulation
데이터를 외부에 직접 노출시키지 않고 메서드를 이용해 보호할 수 있다.
- 변수는 private로 접근 제한.
- getter와 setter 메서드를 통해서 접근하도록 한다.
class UserInfo {
private String name = "대상혁";
private int account = 10000;
public String getName() {
return name;
}
public void setName(String name) {
if(name == null) {
System.out.println("name은 not null!!");
return;
}
this.name = name;
}
public int getAccount() {
return account;
}
public void setAccount(int account) {
if(account < 0) {
System.out.println("account는 음수가 아님!!");
return;
}
this.account = account;
}
}
public class UnbelievableTest {
public static void main(String[] args) {
UserInfo info = new UserInfo();
System.out.printf("사용자 정보:%s, %d%n", info.getName(), info.getAccount());
info.setName(null);
info.setAccount(-1000);
System.out.printf("사용자 정보:%s, %d%n", info.getName(), info.getAccount());
}
}
나. Singleton 디자인 패턴
객체 생성을 제한해야 하는 경우가 있다.
- 여러 개의 객체가 필요 없는 경우. (ex. stateless한 객체)
- 객체를 계속 생성/삭제하는 것이 비용이 커서 재사용이 유리한 경우.
class SingletonClass {
private SingletonClass() { }
private static SingletonClass instance;
public static SingletonClass getInstance() {
if(instance == null) {
instance = new SingletonClass();
}
return instance;
}
public void printInfo() {
System.out.println("Hello World");
}
}
public class SingletonTest {
public static void main(String[] args) {
SingletonClass sc = SingletonClass.getInstance();
sc.printInfo();
SingletonClass sc2 = SingletonClass.getInstance();
sc2.printInfo();
}
}
/*
Hello World
Hello World
*/
private SingletonClass() {}
: 외부에서 생성자에 접근 금지 → 생성자의 접근 제한자를private
으로 설정public static SingletonClass getInstance() {…}
: 더 이상 외부에서 객체를 생성하지 못하기 때문에 객체 생성 없이 접근할 수 있는static
getter
메서드를 추가.
: setter는 불필요private static SingletonClass instance;
: 앞서 추가한static
메서드를 통해서 접근하므로static
으로 선언한다.
외부에서는 언제나 getter를 통해서 객체를 참조하므로 하나의 객체를 재사용.
2. 다형성
하나의 객체가 다수의 형(타입)을 가질 수 있는 성질.
가. 다형성의 장점
상속 관계에 있을 때 조상 클래스의 타입으로 자식 클래스 객체를 레퍼런스 할 수 있다.
SpiderMan s = new SpiderMan();
의s
는 SpiderMan이자 Person이고 Object이다.
Person[] persons = new Person[10];
persons[0] = Person();
persons[1] = new SpiderMan();
확장해서 생각하면 Object
배열 안에는 모든 객체를 담을 수 있다.
이러한 특성을 활용해서 Collection API가 등장함. (ex. ArrayList
)
public class PolymorphismUsage {
public void useObjectArray() {
Object[] objs = new Object[4];
objs[0] = "Hello";
objs[1] = objs;
objs[2] = new SpiderMan();
objs[3] = 3; // Auto Boxing
for(Object obj : objs) {
System.out.println(obj.getClass().getSimpleName());
}
}
public static void main(String[] args) {
PolymorphismUsage usage = new PolymorphismUsage();
usage.useObjectArray();
}
}
/*
String
Object[]
SpiderMan
Integer
*/
objs[3] = 3;
: 3은Integer
로 변환됨.
: 기본형은 담을 수 없음. Wrapper Class로 저장함. 이를 Auto Boxing이라고 한다.
3. 객체의 형변환
가. 다형성과 참조형 객체의 형 변환
objs[2] = new SpiderMan();
...
for(Object obj : objs) {
System.out.println(obj.getClass().getSimpleName());
/*
String
Object[]
SpiderMan
Integer
*/
for
문에서 꺼낸 SpiderMan
의 참조 타입은 Object
이므로 Object
에 정의된 기능만 사용할 수 있다.
SpiderMan
클래스에서 정의한 jump()
나 fireWeb()
등은 사용할 수 없다.
그래서 형변환이 필요가 있다.
나. 참조형 객체의 형 변환
Phone phone = new Phone();
Object obj = phone;
- 하위 타입(Phone)을 상위 타입(Object)으로 형 변환 → 묵시적 형변환
Phone을 생성하면 Object도 생성된다.
Phone phone = new SmartPhone();
SmartPhone sPhone = (SmartPhone)phone;
- 상위 타입(Phone)을 하위 타입(SmartPhone)으로 형 변환 → 명시적 형변환
하지만 Phone을 생성하면 SmartPhone은 생성되지 않는다.
다. instanceof 연산자
무늬만 SmartPhone
인 Phone
은 아무리 SmartPhone
이라고 우겨도 Phone
에 불과하다.
SmartPhone
의 기능을 가지고 있지 않다.
이렇게 조상 클래스를 자식 클래스로 형변환한 경우 컴파일 에러는 발생하지 않지만 런타임 에러가 발생한다. (더 힘듦)
이런 문제를 예방하고자 실제 메모리에 있는 객체가 특정 클래스 타입인지 확인하기 위해서 instaceof
연산자를 사용한다.
SmartPhone smartPhone = new Phone();
if(smartPhone instanceof SmartPhone) { //false
...
}
// smartPhone은 정말로 SmartPhone인가?
실제 객체가 특정 클래스 타입인지 확인하고 true
혹은 false
를 반환한다.
라. Override한 method가 있는 경우
class SuperClass {
String x = "super";
public void method() {
System.out.println("super class method");
}
}
class SubClass extends SuperClass {
String x = "sub";
@Override
public void method() {
System.out.println("sub class method");
}
}
public class MemberBindingTest {
public static void main(String[] args) {
SubClass subClass = new SubClass();
System.out.println(subClass.x);
subClass.method();
SuperClass superClass = subClass;
System.out.println(superClass.x);
superClass.method();
SubClass casted = (SubClass) superClass;
System.out.println(casted.x);
casted.method();
}
}
/*
sub
sub class method
super
sub class method
sub
sub class method
*/
오버라이드된 메서드(method()
)는 자식 클래스의 메서드(SubClass.method()
)가 호출된다.
이는 자바가 메서드를 동적 바인딩하기 때문이다.
이때 오직 Overrride된 메서드만 해당된다.
변수는 해당 없다.
마. 정적 바인딩과 동적 바인딩
- 정적 바인딩 (static binding)
- 컴파일 단계에서 참조 변수의 타입에 따라 연결이 달라짐
- 대상 : 변수, 연산자, static method
- 동적 바인딩 (dynamic binding)
- 다형성을 이용해서 메서드 호출이 발생할 때 runtime에 메모리의 실제 객체의 타입으로 결정
- 대상 : instance method
정적 바인딩 | 동적 바인딩 | |
수행 속도 | 상대적으로 빠름 | 상대적으로 느림 |
메모리 공간 활용 효율 | 상대적으로 높음 | 상대적으로 낮음 |
객체지향적 | 다형성으로 효율적인 코드 재사용 가능 |
바. 용도에 따른 가장 적합한 메서드 구성
public void useJump1(Object obj){
if(obj instanceof Person){
Person casted = (Person) obj;
casted.jump();
}
상위 타입으로 올라갈수록 활용도는 높아지지만 코드의 복잡성도 함께 증가함.
public void useJump2(SpiderMan spiderMan){
if(spiderMan instanceof SpiderMan){
spiderMan.jump();
}
}
그렇다고 최하위 타입(SpiderMan
)으로 내려가면 중간 타입(Person
)은 커버할 수 없음.
public void useJump3(Person person){
person.jump();
}
jump()
가 처음 정의된 Person
타입의 매개변수를 사용하면 Person
뿐만 아니라 SpiderMan
까지도 커버할 수 있다.
사용할 속성이나 메서드가 최초로 정의된 ‘비즈니스 로직 상 최상위 객체’로 형변환하여 사용하자.
4. Object 메서드 재정의
- Object class
- 가장 최상위 클래스로 모든 클래스의 조상
- java.lang.Object
- registerNative(): void
- getClass(): java.lang.Class<?>
- clone(): java.lang.Object
- finalize() void
- (): void
- 대부분 클래스에서 재정의하는 것
- hashCode(): int
- equals(Obj: java.lang.Object): boolean
- toString(): java.lang.String
- 멀티 스레드에서 사용하는 것
- notify(): void
- notifyAll(): void
- wait(timeout: long): void
- wait(timeout: long, nanos int): void
- wait(): void
hashCode()
, equals()
, toString()
정도는 재정의하자.
가. toString()
객체를 문자열로 변경하는 메서드.
System.out.print()
내부적으로 Object.toString()
을 호출한다.
모든 클래스는 Object 클래스를 상속받기 때문에 toString()
메서드를 재정의할 수 있다.
이렇게 재정의하면 System.out.print()
와 같이 Object.toString()
할 일이 있으면 자동으로 재정의된 메서드를 실행한다.
그냥 무조건 만들어라.
나. equals()
1) 필요성
두 객체가 같은지 비교.
public class EqualTest {
public static void main(String[] args) {
int a = 10;
int b = 10;
System.out.println(a == b);
Student s = new Student();
Student s1 = new Student();
System.out.println(s == s1);
}
}
/*
true
false
*/
System.out.println(s == s1);
:==
는 변수에 저장된 값이 같은지 확인한다. 그래서 두 개의 레퍼런스 변수는(s
,s1
)는 주소값 그 자체를 비교한다. 저마다 다른 객체를 참조하기 때문에false
로 판단된다.
public class EqualTest {
public static void main(String[] args) {
String str = new String("java");
String str1 = new String("java");
System.out.println(str == str1);
String str2 = "java";
String str3 = "java";
System.out.println(str2 == str3);
}
}
/*
false
true
*/
String
은 다소 예외적이다.
new
키워드를 사용하지 않고도" "
을 사용해서 인스턴스를 생성할 수 있는다.“java”
를 여러 번 사용했지만“ “
를 사용한 경우 최초 1회만 새로운 인스턴스를 생성하고 그 이후에는 같은 인스턴스를 참조한다.- 하지만
new
키워드를 사용하면 사용한 만큼 인스턴스를 새로 생성한다.
String str = new String("java");
String str1 = new String("java");
System.out.println(str == str1); //false
System.out.println(str.equals(str1)); //true
참조값이 아닌 값 그 자체를 비교하기 위해서 equals()
를 사용해야 한다.
2) 실습
학번이 동일하다면 같은 학생으로 취급한다고 가정하자.
@Override
pubilc boolean equals(Student s) {
return num == s.getNum();
}
// 오류 발생!
Override 시 매개변수도 같아야 한다.
Object.equals(Object obj)
이기 때문에 매개변수의 타입은 무조건 Object
다.
@Override
pubilc boolean equals(Object obj) {
return num == obj.getNum();
}
// 오류 발생!
Object.getNum()
이라는 메서드는 없다.
형변환이 필요하다.
@Override
public boolean equals(Object obj) {
if(obj == null) {
return false;
}
if(!(obj instanceof Student)) {
return false;
}
Student s = (Student) obj;
return (num == s.getNum());
}
if(!(obj instanceof Student)) {
: 런타임 에러를 방지하기 위해서 전달된obj
가Student
클래스로 형 변환할 수 있는지 먼저 확인한다.Student s = (Student) obj;
: 형 변환.return (num == s.getNum());
: 학번num
이 같다면 같은 학생이라고 판단한다.
새로운 클래스를 정의할 때마다 equals()
를 정의하는 것이 좋다.
(참고로 obj1.equals(obj2)
는 obj1.hashCode() == obj2.hashCode()
와 같아서 같은 객체인지만 비교한다.)
다. hashCode()
Student s = new Student(1234, "홍길동");
Student s1 = new Student(1234, "홍길동");
HashSet<Student> set = new HashSet();
set.add(s);
set.add(s1);
System.out.println(set);
//[홍길동 1234, 홍길동 1234]
HashSet
은 원래 중복되는 값을 저장하지 않는다.
하지만 두 번 저장된 것을 볼 수 있다.
이는 중복인지 검사할 때 equals()
뿐만 아니라 hashCode()
도 활용하기 때문이다.
hash 값을 사용하는 Collection(HashMap, HashSet, HashTable)에서 이런 문제를 예방하기 위해서 equals()
를 재정의 할 때는 hashCode()
도 재정의하는 것이 좋다.
@Override
public int hashCode() {
return num;
}
1) Object.hashCode()
Runtime 객체의 유일한 integer
값을 반환한다.
이 값은 Object
클래스에서는 기본적으로 객체의 내부 주소값을 정수로 변환하면서 생성된다.
( 그렇기 때문에 동일한 메모리 주소를 갖는다면 같은 값을 반환받는다.)
2) equals()와의 관계
hashCode()
의 규약을 보면, equals()
와의 관계를 정의 내릴 수 있다.
- 자바 애플리케이션 실행 중 같은 객체에 대한
hashCode()
호출은 항상 같은 정수 값을 반환한다. - 만약 두 오브젝트가
equals()
에서 같다고 판별되었다면, 두 오브젝트의hashCode()
반환 값은 반드시 동일해야 한다. - 두 오브젝트가
equals()
에서 다르다고 판별되었다면, 두 오브젝트의hashCode()
반환 값이 반드시 구분될 필요는 없다. 다만, 서로 다른 객체의 해시 코드가 서로 다르다면 해시 테이블의 성능이 향상되는 것을 개발자가 인지하는 것이 필요하다.
5. 자동완성 이용하기
자동완성 이용하면 편하다.
@Override
public int hashCode() {
return Objects.hash(num);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Student other = (Student) obj;
return num == other.num;
}
'공부 중 > Java' 카테고리의 다른 글
[Java] String (0) | 2024.01.22 |
---|---|
[Java] 인터페이스 (0) | 2024.01.22 |
[Java] 상속 (0) | 2024.01.21 |
[Java] 객체지향 (0) | 2024.01.21 |
[Java] 배열 (0) | 2024.01.21 |