객체지향 프로그래밍 포스팅에서 객체지향의 특징을 몇 가지 살폈었다. Java는 객체지향 언어인만큼 class를 사용하고 있으며 객체지향의 특징 중 encapsulation, inheritance, polymorphism 등이 적용되어 있다. 이 글에서는 Java에서 사용하는 class에 대한 기본적인 몇 가지를 알아볼 것이다.
Java에서 class
앞선 포스팅에서 다음와 같은 내용을 작성했다.
객체란 어떤 개념을 추상화하고 모델링한 요소이다. 객체는 상태(state)와 행위(behavior)를 가지고 있으며 행위를 통해 상호작용한다.
class는 instance를 만들기 위한 설계도이며, instance는 class라는 설계도를 이용해 만들어진 메모리에 할당된 실체이다.
class는 객체를 만들기 위한 설계도인 만큼 state와 behavior를 가지고 있어야 한다. java에서 class에서 state는 attribute(또는 field)로, behavior는 method로 작성하며, 이 둘을 합쳐 member라고 한다.
attribute는 객체의 정보가 저장되며, method는 객체의 behavior를 맡는다. 추가적으로 특별한 method인 constructor는 객체가 생성될 때 초기화 역할을 담당하며 필요 시 attribute의 값을 초기화 해 준다. 생성하지 않을 경우 아무것도 하지 않는 default constructor가 생성된다.
// ClassExample.java
public class ClassExample {
// attribute
private int attribute1;
private String attribute2;
// special method: constructor
ClassExample(){
this.attribute1 = 1;
this.attribute2 = "Class Example";
}
// method
public int getAttribute1() {
return attribute1;
}
public String getAttribute2() {
return attribute2;
}
}
객체의 크기
class를 정의했을 때 메모리에 할당되는 크기, 즉 객체의 크기는 attribute size의 총 합이다. attribute size의 총 합이 곧 instance size이며, 그만큼 heap에 올라간다. 만약 2개를 정의했다면 heap에서는 instance size * 2만큼 할당된다. static으로 선언된 attribute는 metaspace에 저장되므로 객체의 크기에 더해지지 않는다.
method는 class를 정의했을 때 JVM의 metaspace라는 곳에 따로 올라간다.
생성자 Constructor
constructor는 객체가 new 연산자에 의해 생성될 때 자동으로 실행되는 method이며, heap memory에 객체를 할당하고 해당 주소를 리턴한다. 만약 예외 발생 시 객체는 생성되지 않는다.
constructor의 이름은 class 이름과 동일하게 작성해야 한다. 별도로 constructor를 지정해 주지 않은 경우, 생성 시 아무것도 하지 않는 default constructor가 생성된다. parameter constructor를 이용해 class attribute의 값을 초기화한 후 사용할 수 있다.
Method Signature
method signature는 Java에서 method를 구별하는 식별자이다.
이름과 parameter, 2가지 정보를 이용해 method를 구별하며 이름과 parameter가 모두 같아야 같은 method로 취급한다. 즉슨,
- 이름이 같아도 parameter가 다르면 다른 method이다.
- parameter가 같아도 이름이 다르면 다른 method이다.
- 이름과 parameter가 다르면 다른 method이다.
라는 것이다.
parameter의 경우 parameter 이름이 아니라 parameter type/개수/순서로 구분한다. parameter 이름을 떼고 구분한다는 것이다. 예를 들자면
- method(int a, string b)와 method(string a, int b)인 method는 다르다. (parameter가 [int, string]과 [string, int]로 다름)
- method(int a, string b)와 method(int b, string a)는 같다. (parameter가 [int, string]으로 동일)
생성자 오버로딩 Constructor Overloading
overloading은 같은 이름의 method를 여러 개 정의하는 것이다.
Method signature를 왜 살펴봤냐? 바로 constructor overloading 때문이다. 여러 가지 조건에 따라 객체를 다르게 초기화하기 위해 constructor overloading을 할 수 있다. constructor overloading은 method signature가 달라야 한다. 만약 같은 method signature를 가진 method를 정의한다면 중복 method로 컴파일이 되지 않는다.
// ClassExample.java
public class ClassExample {
// attribute
private int attribute1;
protected String attribute2 = "string attribute in class example";
// constructor
ClassExample(){}
ClassExample(String attribute2){ // constructor overloading
this.attribute2 = attribute2;
}
ClassExample(String attribute2, int attribute1){ // constructor overloading
this.attribute2 = attribute2;
this.attribute1 = attribute1;
}
ClassExample(int attribute1){ // constructor overloading
this.attribute1 = attribute1;
}
}
this
class가 커진다면 내부 member(attribute, method)에 접근할 때 혼동이 올 수 있다. 따라서 class 내부에서 해당 class member에 접근할 때 this를 이용해 표현한다.
// ClassExample.java
public class ClassExample {
// attribute
private int attribute1;
// this.attribute2는 ClassExample의 attribute2를 의미
ClassExample(String attribute2){
this.attribute2 = attribute2;
}
}
접근제어자 Access Modifier
객체지향의 encapsulation를 위해 access modifier를 사용하며, 해당 class, attribute, 또는 method에 접근을 제어해 준다. 크게 아래 4개의 access modifier가 있다.
- private
- default (아무것도 하지 않음)
- protected
- public
class의 경우
class에는 public 또는 default만 적용될 수 있다.
- public : 모든 package에서 해당 class로 접근할 수 있다.
- default : 같은 package 내에서 해당 class에 접근할 수 있다. 다른 package에서 해당 class로 접근할 수 없다.
- 아래 코드와 같은 경우 MyPublicClass에서는 MyClass를 선언하고 정의할 수 있지만 다른 package에서 MyClass를 import할 수 없다.
package MyPublicClass
// default
class MyClass {...}
// public
public class MyPublicClass {...}
member의 경우(attribute, method)
- private : class 내부에서만 해당 attribute, 또는 method에 접근할 수 있다. class 외부에서는 접근할 수 없다.
- default : 같은 package 내에서 해당 attribute, 또는 method에 접근할 수 있다. 다른 package에서 해당 attribute, 또는 method에 접근할 수 없다.
- protected : 같은 package 내에서, 그리고 해당 class를 상속한 class라면 해당 attribute, 또는 method에 접근할 수 있다.
- public : 어디에서도 해당 attribute, 또는 method에 접근할 수 있다.
동일성 Identity과 동등성 Equality, hashCode()
identity는 참조 비교이다. 두 변수의 참조값이 같은지(동일한 객체를 가리키는지)이고
equality는 내용 비교이다. 두 객체의 값을 비교해 사용자가 원하는 논리적 동등성을 비교하는 것이며 일반적으로 두 객체가 다른 객체지만 내부에 있는 attribute가 같을 때 두 객체가 equal하다고 표현한다.
그러나 Java의 모든 class는 Object를 상속하기 때문에 별다른 override를 하지 않을 시 Object class의 equals() method가 작동되어 equality를 비교하며, 이 때 equals() method는 ==와 같은 결과를 낸다. 따라서 두 object의 logical equality를 비교하기 위해서는 해당 method를 override해야 한다.
equals()를 구현할 때는 아래와 같은 단계를 따르면 좋다.
- parameter로 받은 것과 현재 object의 주소값이 같은지 검사한다. 주소값이 같으면 같은 object이다.
- object type이 같은지 확인해야 한다 type이 다르다면 다른 object이다.
- 이후에 logical equality를 비교한다.
// EqualExample.java
public class EqualExample {
// attribute
private int attribute1;
private String attribute2;
// special method: constructor
EqualExample(){
this.attribute1 = 1;
this.attribute2 = "string";
}
// for equality, override equals function
@Override
public boolean equals(Object o){
if(this == o) return true; // address 비교
if(o == null || this.getClass() != o.getClass()) return false; // object type 비교
// logical equality 비교
EqualExample equalExample = (EqualExample) o;
return this.attribute1 == equalExample.attribute1 && this.attribute2 == equalExample.attribute2;
}
}
// App.java
public class App {
public static void main(String[] args) throws Exception {
EqualExample identityExample1 = new EqualExample();
EqualExample identityExample2 = new EqualExample();
EqualExample identityExample3 = identityExample1;
System.out.println(identityExample1 == identityExample2); // false : 다른 객체
System.out.println(identityExample1 == identityExample3); // true : 같은 객체
System.out.println(identityExample2 == identityExample3); // false : 다른 객체
System.out.println(identityExample1.equals(identityExample2)); // true : 1과 2는 다른 객체지만 attribute가 같음
System.out.println(identityExample1.equals(identityExample3)); // true : 1과 3은 같은 객체
System.out.println(identityExample2.equals(identityExample3)); // true : 2와 3은 다른 객체지만 attribute가 같음
}
}
hashCode()
hashCode()는 object가 위치해 있는 address를 hash해 추출한 값이다. 만약 equals() method를 overriding 했다면 hashCode() method 또한 overriding하는 것이 좋다. equals() 같이 같은 두 object의 hashCode()값은 같아야 하기 때문이다.
그 이유는 hash 값을 사용하는 collection (HashMap, HashSet, HashTable 등)은 다음과 같은 의사결정 과정을 거치기 떄문이다.
- hashCode() 값이 다름 : 다른 object
- hashCode() 값이 같음 then
- equals() 값이 다름 : 다른 object
- equals() 같이 같음 : 같은 object
이러한 과정을 거치기 때문에 equals() method를 override해도 hashCode() 값이 다르다면 해당 collection에 같은 값이라고 의도하고 넣었지만 실제로는 다른 값이라고 인식해 의도하지 않은 동작이 발생할 수 있다. 따라서 Hash를 사용하는 collection에서 의도하지 않은 동작을 허용하고 싶지 않다면 hashCode() method 또한 override해야 한다.
만약 hashCode() method를 override 했는데 원래의 hashCode() method(object의 address hash value)가 필요한 경우에는 identityHashCode() method를 사용해 얻을 수 있다.
* hashCode()의 예외
hashCode() method는 int를 리턴한다. 따라서 주소값으로 8byte를 사용하는 64bit OS의 JVM에서는 hash 값이 같을 수 있다.
그렇지만 큰 문제는 없다. 만약 equals() method를 override한 상황이라면 사용자가 정의한 equals() method의 비교 연산을 따라 같으면 같은 object로, 다르면 다른 object로 판정할 것이다. equals() method를 override하지 않았더라면 위에서 설명했듯 == 연산자로 실제 address를 비교하기 때문에 address가 같으면 같은 object로, 다르면 다른 object로 판정할 것이다.
'Development > Java' 카테고리의 다른 글
[Java] Polymorphism - static polymorphism, dynamic polymorphism, casting (0) | 2023.02.23 |
---|---|
[Java] Inheritance - access modifier, super, overriding (0) | 2023.02.23 |
[Java] static, final (0) | 2023.02.23 |
[Java] Data types, 예외적인 String, 그리고 call by value (0) | 2023.02.21 |
[Java] HttpURLConnection으로 HTTP 통신하기 (0) | 2022.09.13 |