본 게시글은
data class (tistory.com)

 

data class

pojo : 메소드가 작동을 하는 것이 아니라 비어 있는 틀 역할을 하는 클래스 data class 클래스이름 (변수,변수....) 변수는 class의 property처럼 사용. dataclass 도 init과 메소드 생성 가능 컴파일러가

wldnjs1277.tistory.com

을 토대로 질문이 들어와서 추가 정리 내용을 작성합니다.


Kotlin 의 data class 는, 데이터를 보유하기 위하여 만든 클래스입니다.

 

만약 다음과 같은 조건으로 User 라는 Class 를 만들어야 하는 경우를 생각해 보겠습니다.

1. 이름을 의미하는 문자열 Type 의 name filed 를 포함한다.
2. 나이를 의미하는 정수 Type 의 age filed 를 포함한다.
3. 생성하는 순간에 name 과 age를 parameter 로 전달받고, 전달받은 name 과 age 로 초기화한다.
4. 한번 생성된 User라는 객체의 name 과 age filed 는 read only 이다.
5. 출력을 위하여 toString() 을 호출하는 순간 User{name='value', age=value} 형태로 출력한다.
6. 값을 비교하기 위하여 eqauls() 를 구현한다.

7. 객체의 값이 일치하다면, 같은 주소값을 반환하는 hashCode() 를 구현한다.

 

<Java 로 구현하기>

public class User {
    private final String name;
    private final int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return age == user.age &&
                Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

<Kotlin 으로 구현하기>

data class User(
    val name: String,
    val age: Int
)

 


1. toString()

Java 에서는 기본적으로 객체의 toString() 은 
이름@[16진수로 표시한 hashcode] 를 반환합니다.
만약, 위에서 작성한 User 라는 Java class 에서 hashCode() 를 아래와 같이 수정한다면,

출력결과는 User@0 이 됩니다.
따라서 Java 에서는 toString() 을 Override 하여 원하는 출력결과를 명시해야합니다.
(IDE 가 좋아졌기 때문에, 자동완성 기능으로 조건에서 언급한 기준처럼 작성은 되지만, 반드시 Override 해야하는 것은 변하지 않습니다.)

@Override
public int hashCode() {
    return 0;
}

그러나 Kotlin 의 data class 은 이를 사용자가 직접 작성하지 않더라도, 조건에서 언급한 기준으로 동작합니다.
만약 Kotlin 의 data class 에서 toString() 을 아래와 같이 Override 하는 순간, 
Java 의 객체 toString() 과 동일하게 동작하여
이름@[16진수로 표시한 hashcode] 를 반환합니다.

override fun toString(): String {
    return super.toString()
}


2. eqauls()
Objecet 의 equals() 는 주소값 비교입니다.
즉, 만들어진 객체가 완전히 같은 주소값을 가리키고 있으면 true, 아니면 false 를 반환합니다.

흔히들 Java 의 equals 는 값비교, == 은 주소비교 라고 하지만, 재정의된 Class 에 한에서만 위와 같이 동작합니다.

* String 에서 equals 는 이미 재정의되어 있기 때문에 값비교가 성립하는 것입니다. 모든 Class 가 성립하지 않습니다.

Kotlin data class 는 이러한 equals 가 이미 재정의되어 값비교를 할 수 있게 되어 있습니다.

즉, Kotlin 의 data class 에서 아래와 같이 사용자가 직접 Override 하는 순간,
그 조건이 Object 의 주소비교가 됩니다.

override fun equals(other: Any?): Boolean {
    return super.equals(other)
}

 

3. hashcode()
객체를 식별할 수 있는 값을 반환합니다. (주소값은 유니크해서 일반적으로 주소값을 반환하지만, 주소값이 아닐 수 있습니다. 중요한건 해당 값은 "유니크" 해야 한다는 점입니다.)

"유니크" 하다는 것은 Key <-> Value 로 치면, Key 로 접근했을 때 항상 동일한 Value 에 접근할 수 있어야 한다는 것입니다. 그러나, 주소값만 반환한다면, 논리적으로 동일한 값을 가졌음에도 불구하고 새로 생성하는 객체마다 늘 다른 값을 반환합니다.

이러한 이유로 Java 의 class 는 hashcode 를 Override 하여 논리적으로 같을 경우 같은 hashcode를 반환하도록 작성하고,

Kotlin 의 data class 는 해당경우에 같은 hashcode 를 반환하도록 되어 있습니다.

  Java Class Kotlin data Calss
toString() Override 하지 않으면
이름@16진수 hashcode 반환

Override 할 때
super.toString() 만 작성하면
이름@16진수 hashcode 반환
Override 하지 않으면
자동생성되는 String 을 반환

Override 할 때
super.toString() 만 작성하면
이름@16진수 hashcode 반환
equals() Overrdie 하지 않으면
객체가 주소값이 같은지만 판단

Overrride 할 때
super.equals(other) 만 작성하면
역시 객체가 완전히 같은지만 판단
Override하지 않으면
객체의 값이 같은지를 판단

Override 할 때
super.equals(other) 만 작성하면
역시 객체가 완전히 같은지만 판단
hashcode() Override 하지 않으면
서로 다른 객체의 value 가 같더라도, 다른 유니크값을 반환합니다.

Override 할 때
super.hashCode() 를 작성하면
서로 다른 객체의 value 가 같더라도, 다른 유니크값을 반환합니다.

Override 할 때
value 를 포함하여 작성하면
서로 다른 객체의 value 가 같으면, 같은 유니크값을 반환합니다.
Override 하지 않으면
value 를 포함하도록 작성되어있어서
서로 다른 객체의 value 가 같으면, 같은 유니크값을 반환합니다.

Override 할 때
super.hashCode() 를 작성하면
서로 다른 객체의 value 가 같더라도, 다른 유니크값을 반환합니다.

Override 할 때
value 를 포함하여 작성하면
서로 다른 객체의 value 가 같으면, 같은 유니크값을 반환합니다.

 



* 값이 같은지?? 논리적으로 같은지??
어짜피 equals 가 true 이면 그 값이 동일하다는 건데, hashcode 도 true 아닌가요???????

User 라는 class 와 그 내용이 완전히 같은 User2 라는 클래스가 있다고 가정하겠습니다.
아래처럼 eqauls() 는 값 뿐만 아니라 class 도 확인합니다.
즉 생성할때 object 값이 같더라도, class 가 다르면 서로 다르다고 판단합니다.

그러나, hashCode() 는 논리적으로 같음을 보기 때문에, 생성하는 class 가 다르더라도,
object 값이 같다면, 서로 같은 유니크값을 반환하게 됩니다.

 

 

val a = User("DevHyeon", 28)
val b = User("DevHyeon", 28)
val c = User2("DevHyeon", 28)
//true
println(
    a == b
)
//true
println(
    a.hashCode() == b.hashCode()
)

//false
println(
    a == c
)
//true
println(
    a.hashCode() == c.hashCode()
)


너무나도 헷갈리고, 평소에 자주 사용하지 않는 한 나중에 다시보면 아! 이랬지! 싶은 내용입니다만,
헐? 그런거야? 보다는 아! 이랬지!

(적어도, 분명 두개가 다른건 확실한데.. hashCode() 가 반환하는게 무조껀 주소값이 아니였는데... 정도까지는 알고있는게 좋아보입니다.)

최종 결과 이미지

 

평범한 BottomNavigationView 에 질리셨다면, 이제는 조금 변경해볼까요?

지금부터 순서대로 따라하시면 위와 같은 결과를 얻을 수 있습니다! (6단계의 과정만 진행하시면 됩니다!)

 

 

1. Theme 수정하기
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.YoutubeLayout" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <item name="colorPrimary">#0F9D58</item>
        <item name="colorPrimaryVariant">#0F9D58</item>
        <item name="colorOnPrimary">#000000</item>
    </style>
</resources>
2. Menifest 수정하기
<application
  android:allowBackup="true"
  android:icon="@mipmap/ic_launcher"
  android:label="@string/app_name"
  android:roundIcon="@mipmap/ic_launcher_round"
  android:supportsRtl="true"
  android:theme="@style/Theme.YoutubeLayout">
  <activity android:name=".MainActivity">
    <intent-filter>
      <action android:name="android.intent.action.MAIN" />

      <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
  </activity>
</application>
3. menu Item 추가하기

1. 사전에 Vector Icon 을 만들어주세요 ^_^

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/home"
        android:icon="@drawable/ic_home"
        android:title="Home"/>

    <item
        android:id="@+id/Search"
        android:icon="@drawable/ic_search"
        android:title="Search"/>

    <item
        android:id="@+id/placeholder"
        android:title=""/>

    <item
        android:id="@+id/Profile"
        android:icon="@drawable/ic_favorite"
        android:title="Favorite"/>

    <item
        android:id="@+id/Settings"
        android:icon="@drawable/ic_locker"
        android:title="Locker"/>

</menu>
4. Custom BottomNavigationView 생성하기

이렇게 해주지 않으면, 공백에 해당하는 Item 에도 클릭이벤트가 발생하기 때문에, 간단하게 생성해주세요 :)

class YoutubeBottomNavigationView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : BottomNavigationView(context, attrs, defStyleAttr) {

    init {
        val menuView = getChildAt(0) as ViewGroup
        //index 2 : 비활성화 아이템
        menuView.getChildAt(2).isClickable = false
    }
}
5. MainActivity.xml 수정하기

app:elevation="0dp" 가 왜 필요하죠?
* android:elevation="0dp" 가 아니라, app:elevation="0dp" 라는 점을 주의하세요!

BottomNaviagtionView 에는 기본적으로 app:elevation 이 8dp 로 적용되어있습니다. 따라서, 0dp 로 해주지 않으면 그림자 잔상효과가 남아요!

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.bottomappbar.BottomAppBar
        android:id="@+id/bottomAppBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom">

        <com.devhyeon.youtubelayout.YoutubeBottomNavigationView
            android:id="@+id/bottomNavigationView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginEnd="16dp"
            app:elevation="0dp"
            app:labelVisibilityMode="labeled"
            android:background="@android:color/transparent"
            app:menu="@menu/bottom_nav_menu" />

    </com.google.android.material.bottomappbar.BottomAppBar>


    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="@string/app_name"
        android:src="@drawable/ic_add"
        app:layout_anchor="@id/bottomAppBar" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

 

6. 결과 확인하기

 

다양하게 NavagationView 를 사용해보면 좋을 것 같아요! :)

백준에서 코틀린으로 알고리즘 문제를 풀던 중에, 테스트코드를 직접 입력하고, 출력이 맞는지 눈으로 확인하는 과정이 너무 힘들었다.

 

그래서 테스트 코드를 사용해보고자 하였다.

 

Android 에서 Junit 으로 잠시 사용해본 적은 있기에 어려울 것 같지는 않았으며, 실제 사용해보니 동일? 했다.

 

 

먼저, 테스트를 하고자하는 클래스를 우클릭하면 Test 코드를 쉽게 생성할 수 있다.

 

 

 

그럼 이렇게, 해당 클래스이름 뒤에 Test 가 붙으면서 internal class 가 하나 생성된다.

테스트하고자 메소드는 생성할 때, 체크하는 부분이 있다.

선택하게 되면, 구현체는 없는 메소드가 생성되어있다.

 

그 이후에 테스트하고자 하는 클래스를 생성하고, assertEquals 를 사용하여, 입력에 따른 출력이 내가 생각한 결과와 같는지 비교하면 된다.

 

만약 flase 라면, ERROR 가 발생한다 :)

 

이제 테스트코드를 하나씩 입력하고, 주석으로 가리고 제출하는 등의 번거로운 작업을 피할 수 있게 되었다 홓...

+ Recent posts