2014年6月26日 星期四

Enumeration on Scala and Java

用途

Enumeration 通常用在所謂的狀態值或者屬性,這些值的特性通常都是由多個固定值組成,且不允許工程師任意自行添加新的值。
Ex: Database 有個欄位 is_valid ,設定只能是 01,在程式的寫作過程,就不允許工程師使用或修改成其他值(ex: 2),以免造成資料庫資料的錯亂。
要避免上述的情況發生,就是限制工程師,也因此就有了 Enumeration

最簡單的 Enumeration

Java 版

  • Source
      public enum DirectionJava {
        TOP, DOWN, LEFT, RIGHT;
      }
    
  • javap -p
      Compiled from "DirectionJava.java"
      public final class DirectionJava extends java.lang.Enum<DirectionJava> {
        public static final DirectionJava TOP;
        public static final DirectionJava DOWN;
        public static final DirectionJava LEFT;
        public static final DirectionJava RIGHT;
        private static final DirectionJava[] $VALUES;
        public static DirectionJava[] values();
        public static DirectionJava valueOf(java.lang.String);
        private DirectionJava();
        static {};
      }
    
在 Java 使用 enum 這個 keyword 來宣告即可。從 Java Bytecode 可以發現,其實 enum 底層本身是個 final class, 也因此 enum 不能再繼承其他的 class (Java 是單一繼承)。 Constructor 設計成 private,所以工程師不能隨意自行產生值,只能使用內定的值或 null

Scala 版本

  • Source
      object DirectionScala extends Enumeration {
        val Top, Down, Left, Right = Value
      }
    
  • scalap -private
      object DirectionScala extends scala.Enumeration {
        def this() = { /* compiled code */ }
        val Top : scala.Enumeration.Value = { /* compiled code */ }
        val Down : scala.Enumeration.Value = { /* compiled code */ }
        val Left : scala.Enumeration.Value = { /* compiled code */ }
        val Right : scala.Enumeration.Value = { /* compiled code */ }
      }
    
  • javap -p DirectionScala
      Compiled from "DirectionScala.scala"
      public final class DirectionScala {
        public static scala.Enumeration$Value Right();
        public static scala.Enumeration$Value Left();
        public static scala.Enumeration$Value Down();
        public static scala.Enumeration$Value Top();
        public static scala.Enumeration$Value withName(java.lang.String);
        public static scala.Enumeration$Value apply(int);
        public static int maxId();
        public static scala.Enumeration$ValueSet values();
        public static java.lang.String toString();
      }
    
  • javap -p DirectionScala\$.class
      public static final DirectionScala$ MODULE$;
        private final scala.Enumeration$Value Top;
        private final scala.Enumeration$Value Down;
        private final scala.Enumeration$Value Left;
        private final scala.Enumeration$Value Right;
        public static {};
        public scala.Enumeration$Value Top();
        public scala.Enumeration$Value Down();
        public scala.Enumeration$Value Left();
        public scala.Enumeration$Value Right();
        private DirectionScala$();
    
Scala 的 Enumeration 跟 Java 的很類似,但 Scala 的內定值型別是 Enumeration.Values, 它本身是個 abstract classEnumeration 內有一個實作的 class: protected class Val,如果需要自定 Enumeration 時,就會需要繼承這個 class。

進階版

使用 Java 說明文件上的 Planet 範例

Java 版

public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass;   // in kilograms
    private final double radius; // in meters
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }
    private double mass() { return mass; }
    private double radius() { return radius; }

    // universal gravitational constant  (m3 kg-1 s-2)
    public static final double G = 6.67300E-11;

    double surfaceGravity() {
        return G * mass / (radius * radius);
    }
    double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }
    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: java Planet <earth_weight>");
            System.exit(-1);
        }
        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight/EARTH.surfaceGravity();
        for (Planet p : Planet.values())
           System.out.printf("Your weight on %s is %f%n",
                             p, p.surfaceWeight(mass));
    }
}

Scala 版

Scala 實作的方式很多,網路上都找得到,以下是我自己寫的版本。
object Planets extends Enumeration {

  val G: Double = 6.67300E-11

  final case class Planet private[Planets] (mass: Double, radius: Double) extends Val {
    def surfaceGravity(): Double = G * mass / (radius * radius)
    def surfaceWeight(otherMass: Double): Double = otherMass * surfaceGravity()
  }

  val Mercury = Planet(3.303e+23, 2.4397e6)
  val Venus = Planet(4.869e+24, 6.0518e6)
  val Earth = Planet(5.976e+24, 6.37814e6)
  val Mars = Planet(6.421e+23, 3.3972e6)
  val Jupiter = Planet(1.9e+27,   7.1492e7)
  val Saturn = Planet(5.688e+26, 6.0268e7)
  val Uranus = Planet(8.686e+25, 2.5559e7)
  val Neptune = Planet(1.024e+26, 2.4746e7)


  def main(args: Array[String]) {
    require(args.length == 1, "Usage: java Planet <earth_weight>")

    val earthWeight = args(0).toDouble  
    val mass = earthWeight / Planets.Earth.surfaceGravity

    Planets.values.foreach(p => println("Your weight on %s is %f%n".format(p, p.asInstanceOf[Planet].surfaceWeight(mass))))
  }

}
重點在繼承 Enumeration.Val,由於 Scala 的 values 沒有 generic 的功能,所以需要做轉型,不過 Enumeration 的 values 功能很少用,所以不用太在意。

2014年6月18日 星期三

Scala 與 Java 的差異

前言

以下的內容,討論 Scala 與 Java 在功能與觀念上的不同,不會去比較語法上的不同。

Import

  • Scala 的 import 已有 Java 的 static import 效果
  • Java 的 import org.abc.* 在 Scala 為 import org.abc._ 不過在 Java 及 Scala 都不建議使用;還是詳細寫出 import 那些 class.

Val and Var

使用 val 宣告變數後,就不能再更改其 reference 值;而 var 是允許的,效果等同 Java 宣告。ex:

val a = "abc"
a = "def"

a = "def" 那一行,在 compiler 就會報錯。這項功能有個好處,可以確保變數的 reference 值不會被更動,讓系統的穩定度提高。對工程師在 coding,debug 也有很大的幫助

Getter and Setter in Java Bean on Scala

Scala 在 compile 時,不同的宣告方式,會產生不同 Bean 的 getter 與 setter 的效果。可以使用 scalap -privatejavap -p 來比較 Scala 做了那些事情。

比較 1: 最簡單的 class

class Rational(numer: Int, denom: Int) {

}
  • scalap -private Rational

      class Rational extends scala.AnyRef {
        def this(numer : scala.Int, denom : scala.Int) = { /* compiled code */ }
      }
    
  • javap -p Rational

      public class Rational {
          public Rational(int, int);
      }
    

最簡單的 class,沒有任何功能,也因此 Scala 的 compiler 不會自動產生 member data。

比較 2: 加入 val

class Rational(val numer: Int, val denom: Int) {

}
  • scalap -private Rational

      class Rational extends scala.AnyRef {
        val numer : scala.Int = { /* compiled code */ }
        val denom : scala.Int = { /* compiled code */ }
        def this(numer : scala.Int, denom : scala.Int) = { /* compiled code */ }
      }
    
  • javap -p Rational

      public class Rational {
          private final int numer;
          private final int denom;
          public int numer();
          public int denom();
          public Rational(int, int);
      }
    

很明顯的,加入 val 後, constructor 中的參數,會自動變成 private member data,也產生 getter 的函式:numer()denom()。因為 val 是不可以修改 reference 值,所以 Scala 的 compiler 沒有幫忙產生 setter 的函式

比較 3: 改用 var

class Rational(var numer: Int, var denom: Int) {

}
  • scalap -private Rational

      class Rational extends scala.AnyRef {
        var numer : scala.Int = { /* compiled code */ }
        var denom : scala.Int = { /* compiled code */ }
        def this(numer : scala.Int, denom : scala.Int) = { /* compiled code */ }
      }
    
  • javap -p Rational

      public class Rational {
          private int numer;
          private int denom;
          public int numer();
          public void numer_$eq(int);
          public int denom();
          public void denom_$eq(int);
          public Rational(int, int);
      }
    

由於 var 可以修改 reference,因此 Scala 的 compiler 就自動產生了 setter 的函式:numer_$eq(int)denom_$eq(int)

Add @BeanProperty

Scala 產生的函式,對於給 Java 的使用者,雖然在寫 code 時,是可以使用的,但並不是那麼 friendly。這時候,可以使用 scala 內建的 annotation:@BeanProperty。ex:

import scala.reflect.BeanProperty

class Rational(@BeanProperty var numer: Int, @BeanProperty var denom: Int) {

}
  • scalap -private Rational

      class Rational extends scala.AnyRef {
        var numer : scala.Int = { /* compiled code */ }
        def getNumer() : scala.Int = { /* compiled code */ }
        def setNumer(x$1 : scala.Int) : scala.Unit = { /* compiled code */ }
        var denom : scala.Int = { /* compiled code */ }
        def getDenom() : scala.Int = { /* compiled code */ }
        def setDenom(x$1 : scala.Int) : scala.Unit = { /* compiled code */ }
        def this(numer : scala.Int, denom : scala.Int) = { /* compiled code */ }
      }
    
  • javap -p Rational

      public class Rational {
          private int numer;
          private int denom;
          public int numer();
          public void numer_$eq(int);
          public void setNumer(int);
          public int denom();
          public void denom_$eq(int);
          public void setDenom(int);
          public int getNumer();
          public int getDenom();
          public Rational(int, int);
      }
    

加了 @BeanProperty 後,就產生了常用的 getter 及 setter 了。因此使用 Scala 來產生 JavaBean, 可以讓工程師少寫幾行程式。

Argument

Default Arguement Values

目前 Java 的參數,並沒有預設值功能。也就是說,沒有辦法寫類似 public void func(int a = 0) { } 的程式,而 Scala 卻有這樣功能,因此在程式設計上,就有很大的發揮空間,而且也可以少寫很多程式碼。ex:

class Argu {

  def func(a: Int = 10): Int = a + 1
}
  • scalap -private Argu

      class Argu extends scala.AnyRef {
        def this() = { /* compiled code */ }
        def func(a : scala.Int) : scala.Int = { /* compiled code */ }
      }
    
  • javap -p Argu

      public class Argu {
          public int func(int);
          public int func$default$1();
          public Argu();
      }
    

Scala 在有預設值參數的地方,自行新增一個 function public int func$default$1();,當 func 被呼叫,且不帶入參數時,Scala 的 compile 就會在 bytecode 偷呼叫 func$default$1() 來達到有預設值的效果。

雖然 Scala 允許預設值的參數不一定要放在最後。ex:

def func(a: Int, b:Int = 10, c:Int) { }

但如果不使用 keyword argument 的方式使用函式,就一定都要傳入參數,ex: func()(1, 10, 3) 不能是 func(1,3)。因此在設計程式時,建議還是將 default value 的 argument 放在最後。

Named Arguements

Named arguments 使用跟 python 相同,對於 Java 使用者來說,是一個非常陌生的功能。簡單來說,就是在使用函式時,使用 key-value 方式來傳入參數。 ex:

def func(a: Int, b: Int=10, c:Int) { }

func(a = 10, c = 20)

這項功能,會讓程式有更高的可讀性,也當然自己在寫程式時,參數的命名也不能隨便。如果函式有比較多的參數要傳入時,也可以透過用 key-value 的方式,讓 coding 的速度可以加快。

Lazy Load

Lazy 這項功能,Java 的使用者也是非常陌生的功能,簡單來說,你可以先定義變數的來源,等到真的要使用時,再執行產生變數值。ex:

scala> def func(a: Int): Int = { 
     | println("call func")
     | a + 1
     | }
func: (a: Int)Int

scala> val x = func(10)
call func
x: Int = 11

scala> lazy val y = func(20)
y: Int = <lazy>

scala> val z = y + 10
call func
z: Int = 31

要使用 lazy 的功能,只要在變數前加 lazy 這個 keyword 即可,上述的 sample code 中的 y 就是一個 lazy 變數,在宣告時,並不會真的執行 func(20),要一直到 val z = y + 10 才會真正執行 func(20)

其實 lazy 善用的話,也可以幫工程師少寫一些 code。 ex: 以下是連線資料庫的 sample,用來比較 Java 及 Scala 版的差別。

Java 版

java.sql.Connection conn = null;
java.sql.PreparedStatement stmt = null;
java.sql.ResultSet rs = null;

try {
    conn = getConnection()
    stmt = conn.prepareStatement("...........")
    stmt.setInt(1, 10)
    rs = stmt.executeQuery()

    while(rs.next()) {

    }
}
catch (Exception ex) {
    ex.printStackTrace();
}
finally {
    close(rs);
    close(stmt);
    close(conn);

}

Scala 版

lazy val conn = getConnection()
lazy val stmt = conn.prepareStatement(".......")
lazy val rs = stmt.executeQuery()

try {
  stmt.setInt(1, 10)
  rs = stmt.executeQuery()

  while(rs.next()) {

  }
}
catch {
  case ex: Exception => ex.printStackTrace()
}
finally {
  close(rs)
  close(stmt)
  close(conn)
}