JavaBeans

Conventions

  • The class must have a public default constructor (no-argument). This allows easy instantiation within editing and activation frameworks.
  • The class properties must be accessible using get, set, is (used for boolean properties instead of get) and other methods (so-called accessor methods and mutator methods), following a standard naming convention. This allows easy automated inspection and updating of bean state within frameworks, many of which include custom editors for various types of properties. Setters must receive only one argument.
  • The class should be serializable. It allows applications and frameworks to reliably save, store, and restore the bean's state in a fashion independent of the VM and of the platform.

Bean

The 'beans' of JavaBeans are classes that encapsulate one or more objects into a single standardized object (the bean). This standardization allows the beans to be handled in a more generic fashion, allowing easier code reuse and introspection.

Bean is Reusable Objects

package beans;

/**
 * Class <code>PersonBean</code>.
 */
public class PersonBean implements java.io.Serializable {

    private String name;

    private boolean deceased;
    static final long serialVersionUID = 1L;

    /** No-arg constructor (takes no arguments). */
    public PersonBean() {
    }

    /**
     * Property <code>name</code> (note capitalization) readable/writable.
     */
    public String getName() {
        return this.name;
    }

    /**
     * Setter for property <code>name</code>.
     * @param name
     */
    public void setName(final String name) {
        this.name = name;
    }

    /**
     * Getter for property "deceased"
     * Different syntax for a boolean field (is vs. get)
     */
    public boolean isDeceased() {
        return this.deceased;
    }

    /**
     * Setter for property <code>deceased</code>.
     * @param deceased
     */
    public void setDeceased(final boolean deceased) {
        this.deceased = deceased;
    }
}

Implementing Serializable is not mandatory but is very useful if you'd like to persist or transfer Javabeans outside Java's memory, e.g. in harddisk or over network.

serialVersionUID

If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification. However, it is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected InvalidClassExceptions during deserialization. Therefore, to guarantee a consistent serialVersionUID value across different java compiler implementations, a serializable class must declare an explicit serialVersionUID value. It is also strongly advised that explicit serialVersionUID declarations use the private modifier where possible, since such declarations apply only to the immediately declaring class–serialVersionUID fields are not useful as inherited members. Array classes cannot declare an explicit serialVersionUID, so they always have the default computed value, but the requirement for matching serialVersionUID values is waived for array classes.

If you don't explicitly specify serialVersionUID, a value is generated automatically - but that's brittle because it's compiler implementation dependent.

Places where JavaBeans are used

They often just represents real world data

  • Just a few reasons why JavaBeans should be used
    • They serialize nicely.
    • Can be instantiated using reflection.
    • Can otherwise be controlled using reflection very easily.
    • Good for encapsulating actual data from business code.
    • Common conventions mean anyone can use your beans AND YOU CAN USE EVERYONE ELSE'S BEANS without any kind of documentation/manual easily and in consistent manner.
    • Very close to POJOs which actually means even more interoperability between distinct parts of the system.

Property

  • 프로퍼티란 빈이 관리하는 데이터를 의미한다.
  • 프로퍼티 값을 구하는 메서드는 get 으로 시작한다.
  • 프로퍼티 값을 변경하는 메서드는 set 으로 시작한다.
  • get 과 set 뒤에는 프로퍼티의 이름 첫 글자를 대문자로 변경한다.
  • set 메서드는 1개의 파라미터를 갖는다.
public class Person {
    private String name; // 멤버 변수 or 필드라 부른다.
    
    private getPersonName() {
        return name;
    }
}

위 코드에서 Property 이름은 personName 이다.

Why property are important

  • Jackson 과 같은 대부분의 라이브러리들은 Deserialize 시에 Property 를 사용하여 값을 바인딩 시킨다.

Kotlin Property

class Person {
    // Properties
    var firstName: String = ""
    var familyName: String = ""
    var age: Int = 0

    // Functions
    fun fullName() = "$firstName $familyName"
}

코틀린은 var 로 변수를 선언한 경우 getter/setter 가 자동 생성되기 때문에 class 안에 var 변수는 property 라고 보면된다.

Receiver

프로퍼티는 어떤 클래스의 구체적인 인스턴스와 엮여 있기 때문에 이 인스턴스를 식으로 지정해야 한다.

// p is Instance
fun showFullName(p: Person) = println(p.fullName())

이런 인스턴스(위 코드에서는 p)를 수신 객체(receiver)라고 부르고, 수신 객체는 프로퍼티에 접근할 때 사용해야하는 객체를 지정한다. 클래스 내부에서는 this 로 수신 객체를 참조할 수 있다.

프로퍼티가 사용하는 내부 필드는 항상 캡슐화돼 있고 클래스 정의 밖에서는 이 내부 필드에 접근할 수 없다.

Immutable Property

클래스 프로퍼티는 지역 변수와 마찬가지로 불변일 수 있다. 하지만 이런 경우 초기화를 하는 동안 프로퍼티 값을 지정할 수단이 있어야 한다.

class Person {
    val firstName = "John"
}

val 키워드를 사용해서 만든 프로퍼티를 읽기 전용 프로퍼티(readonly property)라고 부른다.

  • Decompile
public final class Person {
   @NotNull
   private final String name = "Jungho";

   @NotNull
   public final String getName() {
      return this.name;
   }
}

Derived Property

class Person {
    var firstName = "John"
}

var 키워드를 사용해서 만든 읽고 쓸 수 있는 프로퍼티를 파생 프로퍼티(derived property)라고 한다.

Initialize Property by Constructor

생성자를 사용하여 프로퍼티를 초기화 할 수 있다.

class Person(firstName: String, familyName: String) {
    val fullName = "$firstName $familyName"
    
    // init 은 클래스안에 여러개가 존재할 수 있으며, 프로퍼티 초기화 다음으로 실행된다.
    /**
     * init 블록 안에서도 초기화를 시킬 수 있다.
     * 
     * val fullName: String
     * init {
     *   fullName = "BAEKJungHo"
     * }
     */
    init {
        println("Created new Person instance: $fullName")
    }
}
  • Decompile
public final class Person {
   @NotNull
   private final String fullName;

   @NotNull
   public final String getFullName() {
      return this.fullName;
   }

   public Person(@NotNull String firstName, @NotNull String familyName) {
      Intrinsics.checkNotNullParameter(firstName, "firstName");
      Intrinsics.checkNotNullParameter(familyName, "familyName");
      super();
      this.fullName = firstName + ' ' + familyName;
      String var3 = "Created new Person instance: " + this.fullName;
      System.out.println(var3);
   }
}

디컴파일 결과를 보면 프로퍼티가 1개 뿐이라는 것을 알 수 있다. 즉, 코틀린에서 class 나 data class 의 생성자에 있는 파라미터들은 프로퍼티가 아니라는 것이다.(val, var 키워드가 붙은 경우는 프로퍼티가 맞음. 아래에서 설명)

  • 주 생성자 파라미터를 프로퍼티 초기화나 init 블록 밖에서 사용할 수 없다.
class Person(firstName: String, familyName: String) {
    val fullName = ""
    fun printFirstName() { 
        println(firstName) // Error
    }
}
  • 생성자 파라미터 값을 저장할 프로퍼티 만들기
class Person(firstName: String, familyName: String) {
    val firstName = firstName
    fun printFirstName() { 
        println(firstName) // Error
    }
}
  • 생성자 파라미터를 프로퍼티로 만들기
    • 생성자 파라미터 앞에 var 또는 var 키워드를 붙이면 자동으로 프로퍼티를 정의한다.
class Person(private val firstName: String, familyName: String) {
    val fullName = "$firstName $familyName" // constructor parameter call
    
    init {
        println("Created new Person instance: $fullName")
    }
    
    fun printFirstName() {
        println(firstName) // property call
    }
}

Secondary Constructor

부 생성자를 이용해서 프로퍼티를 초기화 시킬 수도 있다.

class Person {
    val firstName: String
    val familyName: String
    
    // Secondary Constructor 에는 val, var 키워드를 사용할 수 없다.
    constructor(firstName: String, familyName: String) {
        this.firstName = firstName
        this.familyName = familyName
    }
}

vararg

// Person[] 타입을 Property 로 갖는 클래스 정의
class Room(vararg val persons: Person)
  • Decompile
public final class Room {
   @NotNull
   private final Person[] persons;

   @NotNull
   public final Person[] getPersons() {
      return this.persons;
   }

   public Room(@NotNull Person... persons) {
      Intrinsics.checkNotNullParameter(persons, "persons");
      super();
      this.persons = persons;
   }
}

Backing field

Backing field 란 프로퍼티의 데이터를 저장해 두는 래퍼런스이다.

// Property
var name: String? = null
    get() = field?.toUpperCase()
    set(value) {
        if (!value.isNullOrBlank()) {
            field = value
        }
    }

Backing field 는 getter/setter 디폴트 구현에 사용되므로, 따로 만들지 않아도 자동으로 생성된다. val 키워드를 이용해서 읽기 전용 프로퍼티를 만들 때는 field 가 만들어지지 않는다.

Property in Interface

프로퍼티는 필드가 아니기 때문에 Interface 에서도 정의할 수 있다.

interface Person {
    val name: String
}

Extension Property

프로퍼티의 본질은 함수이므로 확장 프로퍼티를 만들 수도 있다.

val Context.perferences: SharedPreferences
  get() = PreferenceManager.getDefaultSharedPreferences(this)

프로퍼티를 함수대신 사용할 수 있지만, 완전히 대체하여, 프로퍼티 get/set 안에 분기를 태우는 로직을 구현하거나 알고리즘의 동작을 나타내는 것은 좋지 않다.

  • Bad
// 큰 컬렉션의 경우 답을 찾을 때 많은 계산량이 필요하게 된다. 
// 누구도 게터에 그런 계산량이 필요하다고 예상하지 않는다.
val Tree<Int>.sum: Int
  get() = when (this) {
      is Leaf -> value
      is Node -> left.sum + right.sum
  }

Purpose

  • 프로퍼티는 동작이 아닌 상태를 나타내거나 설정하기 위한 목적으로만 사용되어야 한다.
    • 행동은 함수로 구현
    • 프로퍼티는 상태 집합을 의미

Next

References

  • 코틀린 완벽 가이드 / Aleksei Sedunov 저 / 길벗
  • Kotlin In Action / Dmitry Jemerov, Svetlana Isakova 공저 / 에이콘
  • Effective Kotlin / Marcin Moskala 저 / 인사이트