over 4 years ago

在轉入iOS開發團隊前,有一陣子都在設計相同App的Android版和準備基礎建設。現在的App大都是Internet App,並且在考量UI反應時間、體驗和網路頻寬的消耗,通常也會以某種形式(例如:SQLite或檔案)儲存部分資料在行動裝置上,因此,會頻繁地同步伺服器端和行動裝置端之間資料狀態,再加上行動裝置網路的穩定性不如一般網路可靠(不知4G是否會好一點),需要考慮的事情其實蠻多的,只是後來轉入iOS團隊後(Android版本暫緩),這些設計和想法就暫時放在腦中也沒機會整理下來。

最近,Android版本準備開始復工,趁一些瑣碎的空檔時間,把當初想的結構給整理一下,個人對Model與View分離這件事十分堅持,所以基本的架構概念圖大概就如Figure 1所示,這架構圖很generic,應該適合大多數的Internet App。主要分成幾個色塊,綠色是Model的部分,所有商業邏輯所在,這塊原則上會是platform-independent的設計,所以會以Java SE和Android SDK交集的API完成,也就是說在測試這一塊時,完全不需要Android模擬器,用一般JUnit即可,希望用最少時間達到最大的測試涵蓋率。

對習慣把資料結構直接當成Model的人來說,商業邏輯可能會四散在View或是Controller (這Controller指的是MVC中的Controller)中,但對我來說資料結構只是Model的一部分,也就是圖中Domain Data Model那一塊,以什麼邏輯維持資料結構物件的關係,如何回應Use case或user story所定義的系統訊息,這些處理系統訊息的main controller對我來說,通通都屬於Model,也就是圖中Business Logic Managers,責任是透過Web Service Interfaces和伺服器溝通,然後維持資料結構物件間的關係,最後透過DAO Interfaces將狀態永存(Persistence)在行動裝置上。

Figure 1 - Android App Conceptual Architecture

橘色是platform-dependent的實作,例如用Android的SQLite API來實作DAO,然後以Setter InjectionInterface Injection的方式,將platform-dependent的實作注入,為了做到這點,綠色區塊不能直接依賴platform-dependent的實作,因此Web Service Interfaces和DAO Interfaces兩個區塊只定義介面沒有實作,由橘色區塊中的Web Service ImplementationDAO Implementation分別實作這兩個介面,藉此將依賴關係反轉(Dependency inversion),因此也容易把Web Service Interfaces和DAO Interfaces的mock object給Inject進去,方便測試。

橘色區塊還有一個責任是扮演Android Service的角色,Android可以有Service在背景執行,以接下來要開發的App類型來說,能有在背景執行的Service非常有用。當Activity被喚起,只需bind service就能取得所有最新的狀態。

DAO Implementation是platform-dependent的,所以用橘色標示沒什麼大問題,但Web Service Implementation卻是被標示為紫色,主要是因為以JSON格式傳遞資料的Restful Web Services,是有機會只用以Java SE和Android SDK交集的API,搭配Android上也能使用的JSON Library來完成,也就是Web Service Implementation不一定是platform-dependent的實作,有機會能用JUnit來測試。

淺灰色區塊即View的部分,主要是圖中的Android Activities & UI Flow Controls區塊,這塊測試需要Android模擬器或是使用實機,測試時間也較長,但App好不好用,這塊佔了很大的因素。此外,綠色區塊和橘色區塊的API會以synchronous的方式設計,為了不卡住UI,Android SDK本身有一些Asynchronous的輔助類別(例如AsyncTask),但我覺得還不夠好用,所以Asynchronous Supports & UI Components會根據App的體驗需求客製化一些輔助類別,另外提供一些特殊的UI元件,此外,Android不允許非在UI Thread執行中的程式可以變更畫面,所以Asynchronous Supports & UI Components這區塊還要負責將不同Thread回來的更新通知轉到UI Thread中。這個區塊以藍色標示,主要是因為這些元件應該設計成能跨專案共用的,當成是公司的資產,在開發新專案時就能夠使用才是長久之計。

大原則是這樣沒錯,不過準備開發的App會整合一個第三方的遊戲引擎,到時架構可能會有些許調整,例如在Domain Model或Android Service中多一些Interface與遊戲引擎的部分溝通,盡可能用IoC的方式保護Domain Model。最後,為了不讓不同層級的物件相互汙染,應該會用maven (或gradle)的module機制將不同層級的物件歸屬到不同的module中,再利用dependency的方式限制module之間的可見度。希望Android版在需求相對明確(已有iOS版)的情況下,開發能夠順利些。

 
over 4 years ago

I discussed Java for several weeks, but in my job, the primary language I used is Objective-C in the recent one year. When I was still at school, I studied Objective-C for fun, but never used it in real projects. Thus, I didn't realize the interesting part of Objective-C. After I used Objective-C to develop projects in my job, I found that Objective-C is a charming language. It is a static-typed and complied language, but any message sending (calling a method) is determined at runtime, therefore, it also provides many features usually visible in the dynamic languages. It is an object-oriented language, and has @protocol (as the Interface in UML) and @interface (as the Class in UML), but does not provide obvious mechanism to control the access of an encapsulation, for example, the today's topic: protected methods.

To discuss the topic is because I am pair programming with two programmers new to Objective-C, and both of them asked me the same question: how to declare and use protected methods in Objective-C? For Java, C++, or C# programmers, using protected methods is just as nature as drinking water. I was not used to the absence of protected methods in Objective-C (although Objective-C has @public, @protected, and @private modifiers, they can only be used on member data), same as JavaScript. For the question, I reviewed the projects I developed in the recently years, and I found that protected methods become less. It may due to that I used lots of interface declaration and implementations, and reduced the depth of class hierarchy.

Figure 1 is a common-seen class diagram. In the figure, the orange dash line represents a boundary -- inside the boundary is a system or a package, and outside the boundary is the outer system. In the package, AbstractClass implements the interfaceMethod() method of the SomeInterface interface, and then add an abstract method protectedMethodB(). ConcreteClass extends AbstractClass to implement protectedMethodB(). Then, in the outer system, Context uses the ConcreteClass and a customized CustomizedClass. In such a design, Context can only see two methods: interfaceMethod() and publicMethod(). Only AbstractClass can see all methods. For ConcreteClass and CustomizedClass, the methods other than privateMethod() are visible.

Figure 1 - A class hierarchy

The programming languages trends to be dynamic languages. Therefore, the visibility is usually checked at compile time. Using Java as an example, calling private methods directly in code will get a compiler error, but with the reflection API, calling private methods indirectly is allowed. Same as Objective-C, the methods declared in the Header file are public methods, and the methods only placed in the Implementation file are private methods, but both can be invoked through the method objc_msgSend provided in Objective-C Runtime. Therefore, the simulation discussed here is to simulate the visibility of protected methods only at compile time.

The simulation uses the extension feature -- separate the header file into two parts. One part is AbstractClass.h as shown in Code List 2 that imports the SomeInterface in Code List 1 and declares the publicMethod() method. Another part is AbstractClass_Protected.h that declares the protectedMethodA() and protectedMethodB() methods as shown in Code List 3. Then, in Code List 4, AbstractClass.m imports AbstractClass_Protected.h and implements the interfaceMethod(), publicMethod(), protectedMethodA(), and privateMethod() methods. Objective-C does not have the concept of abstract methods, so AbstractClass.m throws exception to simulate the abstract methods.

Code List 1 - The SomeInterface.h
#import <Foundation/Foundation.h>

@protocol SomeInterface <NSObject>

- (void)interfaceMethod;

@end
Code List 2 - The AbstractClass.h
#import <Foundation/Foundation.h>

#import "SomeInterface.h"

@interface AbstractClass : NSObject<SomeInterface>

- (void)publicMethod;

@end
Code List 3 - The AbstractClass_Protected.h
#import "AbstractClass.h"

@interface AbstractClass ()

- (void)protectedMethodA;

- (void)protectedMethodB;

@end
Code List 4 - The AbstractClass.m
#import "AbstractClass.h"

#import "AbstractClass_Protected.h"

@implementation AbstractClass

- (void)interfaceMethod {
    NSLog(@"interface method invoked");
}

- (void)publicMethod {
    NSLog(@"public method invoked");
    [self protectedMethodA];
    [self protectedMethodB];
    [self privateMethod];
}

- (void)protectedMethodA {
    NSLog(@"protected method A invoked");
}

- (void)protectedMethodB {
    [NSException raise:@"AbstractMethodInvokedException" format:@"Abstract method shouldn't be invoked"];
}

- (void)privateMethod {
    NSLog(@"private method invoked");
}

@end

So far so good. And then discuss how to make the protectedMethodA() and protectedMethoB() methods visible in the inherted classes. ConcreteClass imports AbstractClass_Protected.h, overrides the protectedMethodB() method, as shown in Code List 5 and Code List 6, and in the overridden method publicMethod(), both the methods protectedMethodA() and protectedMethodB() can be used directly. The problem seems to be solved. Well, only a part of problem is solved. Another problem is derived from the extension: should I open AbstractClass_Protected.h to the outer system?

Code List 5 - The ConcreteClass.h
#import "AbstractClass.h"

@interface ConcreteClass : AbstractClass

@end
Code List 6 - The ConcreteClass.m
#import "ConcreteClass.h"

#import "AbstractClass_Protected.h"

@implementation ConcreteClass

- (void)publicMethod {
    NSLog(@"overridden protected method A invoked");
    [self protectedMethodA];
    [self protectedMethodB];
}

- (void)protectedMethodB {
    NSLog(@"ConcreteClass protected method B invoked");
}

@end

Like C++ language, a class is separated into a header file and a implementation file. Although the header file exposes the private methods (variables) to the user, the compiler will check the legality of the method access. In C# or Java language that does not have header files, the visibility is compiled into the bytecode. So while using a packed DLL file or JAR file, the visibility of a method can be checked by the compiler. However, the visibility of a method is Objective-C is determined by whether the header file that have the metod delcaration can be accessed or not. That is if AbstractClass_Protected.h is opened to the outer system, not only CustomizedClass can see the protectedMethodA and protectedMethodB methods, but also Context. Otherwise, neither CustomizedClas nor Context can see the protected methods.

Table 1 lists the comparsion of the protected methods visibility simulated by opened and closed extension. In the table, 'o' means visible, and 'x' means invisible. The first column lists the different class, the second column lists the expected visibility (protected methods are invisible only in Context), the third column lists the actual visibility if the extension is opened (protected methods are visible in all classes), and the forth column lists the actual visibility if the extension is closed to outer system (protected methods are invisible in the Context and CustomizedClass). It is unfortunate that the extension can simulate the protected methods, but not perfectly. The trade-off to open or not open the extension depends on the actual situation. In fact, opening the extension header file is the same as declaring the protected methods in the original header file, so I prefer not to open the extension header file.

Table 1 - Protected methods visibility comparison

Class Expected Opened Extension Closed Extension
Context x o x
AbstractClass o o o
ConcreteClass o o o
CustomizedClass o o x
 
over 4 years ago

連續幾週都是討論Java,但事實上工作一年多來主要使用的語言都是Objective-C。在學時曾因好奇自學Objective-C,但沒有開發過實際的專案,所以沒有體會到Objective-C語言的有趣之處,工作後真正使用Objective-C開發專案,才發現Objective-C是一個很有趣的語言。它是靜態型別編譯式語言,可是所有訊息(呼叫函式)的傳送都是在執行期間才決定而非編譯期間,因此提供很多動態語言才有的特性;它是物件導向語言,有@protocol (UML中的Interface)和@interface (UML中的Class),卻沒有明顯的封裝存取控制,例如今天要討論的主角:protected methods。

會討論這玩意,是因為最近和兩位Objective-C的新人一起pair programming,兩位都問到:怎麼在Objective-C中使用protected methods?對於習慣Java、C++或C#的programmer來說,protected methods是再自然不過的事,但和一開始我不習慣JavaScript一樣,Objective-C並不沒有protected methods (Objective-C雖然有@public@protected@private關鍵字,但只用在修飾成員變數)。這一問,我回頭看自己近幾年的Java專案,我發現我使用protected methods的機會越來越少,可能跟我大量使用interface宣告與實作和減少繼承的深度有關。

Figure 1是一個常見的類別圖,橘色虛線代表一個邊界,虛線內屬於同一個套件或系統,虛線外則是外部系統,首先看套件內,AbstractClass實作SomeInterfaceinterfaceMethod(),然後留下protectedMethodB()這個抽象函式,ConcreteClass繼承AbstractClass並提供protectedMethodB()的實作。接著看外部系統,Context使用ConcreteClass和額外擴充的CustomizedClass。最後,Context實際上能看見的,就只有interfaceMethod()publicMethod()兩個函式。對AbstractClass來說能看到所有的函式,對ConcreteClassCustomizedClass而言,可以看到privateMethod()以外的所有函式。

Figure 1 - A class hierarchy

現在的語言趨勢都是朝向動態語言,因此所謂的可見(Visibility)大多是只在編譯期間的檢查,以Java為例,直接使用private method會出現編譯錯誤,但透過reflection API,呼叫private method是可行的。而Objective-C也是如此,宣告在標頭檔(Header file)中為public methods,直接放在實作檔(Implementation file)中為private methods,但不論是public methods或private methods都能透過Objective-C Runtime所提供的objc_msgSend進行呼叫。所以這裡所討論的模擬,也僅限於讓編譯器檢查時,能夠模擬protected methods的效果

模擬的方法即是使用Extension,將標頭檔分成兩份,一份是Code List 2所示的AbstractClass.h,引入Code List 1所宣告的SomeInterface以及宣告publicMethod(),另一份是Code List 3的AbstractClass_Protected.h,用來宣告 protectedMethodA()protectedMethodB(),接著Code List 4中AbstractClass.m引入AbstractClass_Protected.h後,提供interfaceMethod()publicMethod()protectedMethodA()privateMethod()的實作,Objective-C沒有抽象函式的概念,所以AbstractClass.m是以拋出例外的方式模擬抽象函式。

Code List 1 - The SomeInterface.h
#import <Foundation/Foundation.h>

@protocol SomeInterface <NSObject>

- (void)interfaceMethod;

@end
Code List 2 - The AbstractClass.h
#import <Foundation/Foundation.h>

#import "SomeInterface.h"

@interface AbstractClass : NSObject<SomeInterface>

- (void)publicMethod;

@end
Code List 3 - The AbstractClass_Protected.h
#import "AbstractClass.h"

@interface AbstractClass ()

- (void)protectedMethodA;

- (void)protectedMethodB;

@end
Code List 4 - The AbstractClass.m
#import "AbstractClass.h"

#import "AbstractClass_Protected.h"

@implementation AbstractClass

- (void)interfaceMethod {
    NSLog(@"interface method invoked");
}

- (void)publicMethod {
    NSLog(@"public method invoked");
    [self protectedMethodA];
    [self protectedMethodB];
    [self privateMethod];
}

- (void)protectedMethodA {
    NSLog(@"protected method A invoked");
}

- (void)protectedMethodB {
    [NSException raise:@"AbstractMethodInvokedException" format:@"Abstract method shouldn't be invoked"];
}

- (void)privateMethod {
    NSLog(@"private method invoked");
}

@end

到目前為止,都沒有太大的問題,接著就是繼承的類別能不能看到protectedMethodA()protectedMethoB()了,系統內部的ConcreteClass如Code List 5和Code List 6所示,在引入AbstractClass_Protected.h後,在覆寫的publicMethod()中能夠使用protectedMethodA()protectedMethodB(),並且覆寫protectedMethodB()。似乎解決問題了,但其實只解決了一部分,衍生的問題是AbstractClass_Protected.h要不要開放給外部系統?

Code List 5 - The ConcreteClass.h
#import "AbstractClass.h"

@interface ConcreteClass : AbstractClass

@end
Code List 6 - The ConcreteClass.m
#import "ConcreteClass.h"

#import "AbstractClass_Protected.h"

@implementation ConcreteClass

- (void)publicMethod {
    NSLog(@"overridden protected method A invoked");
    [self protectedMethodA];
    [self protectedMethodB];
}

- (void)protectedMethodB {
    NSLog(@"ConcreteClass protected method B invoked");
}

@end

同樣分標頭檔和實作檔的C++語言,雖然標頭檔會暴露private methods,但編譯器會檢查存取的合法性;而像C#或Java這類沒有標頭檔的語言中,可見度會被編進bytecode中,所以當拿到一個打包過後的DLL檔或JAR檔,還是能知道每個函式的可見度並作檢查。但對Objective-C而言,函式的可見度只依賴能否存取擁有函式宣告的標頭檔,也就是說若開放AbstractClass_Protected.hCustomizedClass能看見protectedMethodAprotectedMethodB,但同樣地,Context也看的到。反之,若不開放,CustomizedClasContext都看不到。

Table 1將用Extension模擬protected methods在開放與不開放的可見度做個比較,o表示可見,x表示不可見,第一欄列出不同的類別,第二欄是普遍預期的可見度(只有Context看不到),第三欄則是開放Extension標頭檔後的可見度(全都看的到),第四欄則是不開放Extension標頭檔的可見度(ContextCustomizedClass看不到)。很可惜,用Extension無法百分之百模擬protected methods,至於開不開放,我想就留給使用的人依據個別情況決定。事實上,開放Extension標頭檔和直接把protected methods宣告在原本標頭檔意義上是差不多的,所以我個人是傾向不開放。

Table 1 - Protected methods visibility comparison

Class Expected Opened Extension Closed Extension
Context x o x
AbstractClass o o o
ConcreteClass o o o
CustomizedClass o o x
 
over 4 years ago

Since I was laid-back recently, today only a small feature is discussed. Comparing the API Document of the utility class Arrays in J2SE 7 and J2SE 8, several static overloading methods are added: stream(T[]), setAll(T[], IntFunction<T>), parallelSetAll(T[], IntFunction<T>), parallelPrefix(T[], BinaryOperator<T>), spliterator(T[] array) and parallelSort(T[], Comparator<T>). The stream(T[]) method has been discussed in the previous article, and it wraps an array as a stream object.

Both the methods setAll(T[], IntFunction<T>) and parallelSetAll(T[], IntFunction<T>) provide the same functionality: using the return value of IntFunction to initialize an array. The IntFunction accepts one integer as parameter, i.e., the index in the array, and returns a value based on the index. These two methods are useful to generate a sequence of numbers, e.g., arithmetic sequence, geometric sequence, etc. The difference between these two methods is the later one processes the array elements in parallel that make the later one faster (but with more resources).

Code List 1 - Use setAll(T[], IntFunction) to generate the arithmetic and geometric sequences
public static int[] generateArithmeticSequence(int initValue, int diff, int length) {
    int[] sequence = new int[length];
    Arrays.setAll(sequence, index -> {
        return index == 0? initValue : initValue + (index - 1) * diff;
    });
    return sequence;
}

public static int[] generateGeometricSequence(int initValue, int factor, int length) {
    int[] sequence = new int[length];
    Arrays.setAll(sequence, index -> {
        return (int)(initValue * Math.pow(factor, index));
    });
    return sequence;
}

The method parallelPrefix(T[], BinaryOperator<T>) uses the previous element (as the left value of the binary operator) and the current indexed element (as the right value of the binary operator) to calculate the return value for the indexed element (something interesting is that Arrays does not provide non-parallel method). Since the return value is updated to the original array, the left value of the binary operator is the updated value. The example shown in the offical document is that given an array [2, 1, 0, 3] and a BinaryOperator<T> to perform addition, the resulting array is [2, 3, 3, 6]. 6 is calculated from the third element (left value) and the forth element (right value) and replaced the forth element.

The spliterator(T[] array) method is used to split the array. I guess that when parallelSort(T[], Comparator<T>) is called or the parallel() method of the stream object returned by the stream(T[] array)is called, the internal implementation uses spliterator(T[]) method to obtain the Spliterator object that manages of array separation for parallel processing.

The new added methods of Arrays are almost discussed and the left one is the today's topic: parallelSort(T[], Comparator<T>). The Arrays class provides methods for sorting for many years. These methods use merge sort algorithm to sort the given array, and the only needed is the Comparator to determine the ordering of two objects. The new added methods parallelSort(T[], Comparator<T>) sort elements (still with merge sort algorithm) in parallel. Therefore, I want to know how much performance can the parallel version improve?

As shown in Code List 2, an interface SortStrategy<T> is declared first. Then, in Code List 3 and Code List 4, the SimpleSortStrategy implementation provides the non-parallel sorting, and the ParallelSortStrategy implementation provides the parallel sorting, respectively. And in Code List 5, a method called generateRandomValues(int) that can generate a given sized array full-filled with random values.

Code List 2 - The SortStrategy interface
package java8.arrays;

import java.util.Comparator;

public interface SortStrategy<T> {

    public void sort(T[] values, Comparator<T> comparator);
}
Code List 3 - The SimpleSortStrategy implementation
package java8.arrays;

import java.util.Arrays;
import java.util.Comparator;

public class SimpleSortStrategy<T> implements SortStrategy<T> {

    @Override
    public void sort(T[] values, Comparator<T> comparator) {
        Arrays.sort(values, comparator);
    }
}
Code List 4 - The ParallelSortStrategy implementation
package java8.arrays;

import java.util.Arrays;
import java.util.Comparator;

public class ParallelSortStrategy<T extends Comparable> implements SortStrategy<T> {

    @Override
    public void sort(T[] values, Comparator<T> comparator) {
        Arrays.parallelSort(values, comparator);
    }
}
Code List 5 - Random sequence generation
public static List<Integer> generateRandomValues(int size) {
    ArrayList<Integer> values = new ArrayList<Integer>();
    for(int index = 1; index <= size; index++) {
        values.add(index);
    }
    Collections.shuffle(values);
    return values;
}

All preparation is completed to examine the performance difference between the non-parallel and parallel sorting. In Code List 6, through the argument setting, the program can run many times with the specified initial array size. Each run, the array size is doubled, so we can discover the relationship between the array size and improvement, e.g., the more elements to sort, the more improvement can make. Since the amount of comparison and swapping affects the sorting time, Code List 6 uses generateRandomValues(int) to generate a list full-filled with random values, and before each strategy starting to sort, the list is transformed to an array (the transformation time is not included) to ensure that each strategy sorts the same random sequence. The environment is similar to the previous experiment Lazy Evaluation & Parallel Stream. The only difference is that the JVM is upgraded from 1.8.0-b132 to 1.8.0_05-b13.

Code List 6 - The experiment scenario
public static void main(String[] arguments) {
    int testRuns = 5;
    int startArraySize = 1000000;
    if(arguments.length == 2) {
        testRuns = Integer.parseInt(arguments[0]);
        startArraySize = Integer.parseInt(arguments[1]);
    }

    List<SortStrategy> strategies = new ArrayList<SortStrategy>();
    strategies.add(new SimpleSortStrategy());
    strategies.add(new ParallelSortStrategy());
    for(int testRun = 1, arraySize = startArraySize; testRun <= testRuns; testRun++, arraySize *= 2) {
        List<Integer> values = generateRandomValues(arraySize);
        for(SortStrategy strategy : strategies) {
            Integer[] array = values.toArray(new Integer[0]);
            long startTime = System.currentTimeMillis();
            strategy.sort(array, new IntegerComparator());
            long usedTime = System.currentTimeMillis() - startTime;
            System.out.println(strategy.getClass().getSimpleName() + " used " + usedTime + " ms to sort " + arraySize + " elements");
        }
    }
}

The result is listed in Table 1. The test run seven times, the array size is from 1 M to 64 M, but the seventh test (64 M) failed because of the OutOfMemoryError. Therefore, Table 1 only lists six records.

Table 1 - The experiment result

Strategy 1 M 2 M 4 M 8 M 16 M 32 M
SimpleSortStrategy 539 760 1781 4120 8700 23016
ParallelSortStrategy 200 286 663 1515 3374 7724
2.70x 2.66x 2.69x 2.72x 2.58x 2.98x

From Table 1, it is obvious that parallelSort(T[], Comparator<T> can improve about 2.5 times to almost 3 times of performance. Therefore, to sort huge size of data, it is recommended to use parallelSort(T[], Comparator<T>.

 
over 4 years ago

最近比較懶一點,所以今天還是只看一個小東西。比較Arrays這個工具類別在J2SE 7J2SE 8的API文件,會發現Arrays多了幾組靜態函式:stream(T[])setAll(T[], IntFunction<T>)parallelSetAll(T[], IntFunction<T>)parallelPrefix(T[], BinaryOperator<T>)spliterator(T[] array)parallelSort(T[], Comparator<T>)stream(T[])先前已經介紹過了,這裡只是把一個陣列包裝成stream來使用。

setAll(T[], IntFunction<T>)parallelSetAll(T[], IntFunction<T>)的功能是一樣的:用IntFunction的回傳值填滿整個array,IntFunction接受一個整數作為參數(即array的索引值)。這類函式在產生特定數列時挺好用的,例如用來產生等差數列或等比數列。兩個函式只差在後者是以平行處理的方式填滿array,速度較快(也較耗資源)。

Code List 1 - Use setAll(T[], IntFunction) to generate the arithmetic and geometric sequences
public static int[] generateArithmeticSequence(int initValue, int diff, int length) {
    int[] sequence = new int[length];
    Arrays.setAll(sequence, index -> {
        return index == 0? initValue : initValue + (index - 1) * diff;
    });
    return sequence;
}

public static int[] generateGeometricSequence(int initValue, int factor, int length) {
    int[] sequence = new int[length];
    Arrays.setAll(sequence, index -> {
        return (int)(initValue * Math.pow(factor, index));
    });
    return sequence;
}

parallelPrefix(T[], BinaryOperator<T>)可以用目前索引的前一個數值(binary operator的left值)和目前索引所指的值(binary operator的right值)計算出新的數值(有趣的是,Arrays沒提供非parallel的版本),回傳值會填入原陣列中,所以binary operator的left值都會是變更後的值。官方的例子假設元數列式[2, 1, 0, 3],然後BinaryOperator<T>做加法運算,則運算後的數列是[2, 3, 3, 6],6是根據陣列的第三個數(left值)加上第四個數(right值)取代原有的第四個數。

spliterator(T[] array)是用來切割陣列,我猜當呼叫parallelSort(T[], Comparator<T>)或是用stream(T[] array)取得stream物件後,再次呼叫parallel()時,內部會用`spliterator(T[])取得Spliterator物件管理陣列的切割,用來做平行處理。

Arrays的其他函式都介紹完了,該看今天的重點:parallelSort(T[], Comparator<T>)。在很早之前Arrays就有提供排序的函式,內部用的是merge sort演算法,使用時只需要提供比較任意兩物件順序的Comparator即可,這次新增的parallelSort(T[], Comparator<T>)就是平行處理的版本,所以我比較想知道平行處理版本所帶來的效能增益有多少?

這裡先如Code List 2宣告一個SortStrategy<T>的介面,然後在Code List 3及Code List 4中分別提供使用非平行處理的SimpleSortStrategy實作和使用平行處理的ParallelSortStrategy實作。接著Code List 5提供一個函式generateRandomValues(int)產生指定數量的亂數數列,

Code List 2 - The SortStrategy interface
package java8.arrays;

import java.util.Comparator;

public interface SortStrategy<T> {

    public void sort(T[] values, Comparator<T> comparator);
}
Code List 3 - The SimpleSortStrategy implementation
package java8.arrays;

import java.util.Arrays;
import java.util.Comparator;

public class SimpleSortStrategy<T> implements SortStrategy<T> {

    @Override
    public void sort(T[] values, Comparator<T> comparator) {
        Arrays.sort(values, comparator);
    }
}
Code List 4 - The ParallelSortStrategy implementation
package java8.arrays;

import java.util.Arrays;
import java.util.Comparator;

public class ParallelSortStrategy<T extends Comparable> implements SortStrategy<T> {

    @Override
    public void sort(T[] values, Comparator<T> comparator) {
        Arrays.parallelSort(values, comparator);
    }
}
Code List 5 - Random sequence generation
public static List<Integer> generateRandomValues(int size) {
    ArrayList<Integer> values = new ArrayList<Integer>();
    for(int index = 1; index <= size; index++) {
        values.add(index);
    }
    Collections.shuffle(values);
    return values;
}

事前準備完後就可以測試有沒有平行處理的差異了,Code List 6的程式可以透過參數決定陣列的最小數量,以及測試要跑幾次,每次都會將陣列的數量加倍,藉此觀察加速是否和數量有關,例如數量越多加速越多?由於比較的次數和交換的次數都會影響到時間,所以Code List 6先用generateRandomValues(int)產生一個亂數list,每種strategy執行前才轉為陣列(轉換不列入時間計算),確保每個strategy都是用到相同亂數順序的陣列。測試的環境和先前測試Lazy Evaluation & Parallel Stream相似,唯一的不同點是JVM從1.8.0-b132升級到1.8.0_05-b13。

Code List 6 - The experiment scenario
public static void main(String[] arguments) {
    int testRuns = 5;
    int startArraySize = 1000000;
    if(arguments.length == 2) {
        testRuns = Integer.parseInt(arguments[0]);
        startArraySize = Integer.parseInt(arguments[1]);
    }

    List<SortStrategy> strategies = new ArrayList<SortStrategy>();
    strategies.add(new SimpleSortStrategy());
    strategies.add(new ParallelSortStrategy());
    for(int testRun = 1, arraySize = startArraySize; testRun <= testRuns; testRun++, arraySize *= 2) {
        List<Integer> values = generateRandomValues(arraySize);
        for(SortStrategy strategy : strategies) {
            Integer[] array = values.toArray(new Integer[0]);
            long startTime = System.currentTimeMillis();
            strategy.sort(array, new IntegerComparator());
            long usedTime = System.currentTimeMillis() - startTime;
            System.out.println(strategy.getClass().getSimpleName() + " used " + usedTime + " ms to sort " + arraySize + " elements");
        }
    }
}

結果列於Table 1,測試的陣列大小從1百萬個整數起跳,跑了7個測試到64百萬個數字,但跑第七個測試(64百萬個數字)時會拋出OutOfMemoryError而失敗,因此Table 1只列到排序32百萬個數字的六筆比較數據。

Table 1 - The experiment result

Strategy 1 M 2 M 4 M 8 M 16 M 32 M
SimpleSortStrategy 539 760 1781 4120 8700 23016
ParallelSortStrategy 200 286 663 1515 3374 7724
2.70x 2.66x 2.69x 2.72x 2.58x 2.98x

從Table 1很明顯可以看到parallelSort(T[], Comparator<T>大概可以帶來2.5倍到接近3倍的效能增益(和數量無關)。所以,結論是當需要處理大量資料的排序時,真的可以考慮使用parallelSort(T[], Comparator<T>

 
over 4 years ago

The topoc today is about a samll change - finally, Java 8 added the Base64 encodder/decoder into the java.util package. Although Base64 increases the actual data transmission length, in the plain-text Internet protocols, Base64 is commonly used to encode the binary data as text (e.g, MIME email). Therefore, it is surprised that the Base64 support is added until Java 8. Before Java 8, it needs the third-party library, for example, Apache Commons Codec, to provide Base64 encoder and decoder.

In the past work, I was developing a system X, a job is to receive the user uploaded file A. For some reason, system X does not have the capacity for storing files, and the uploaded file is actually stored in another system Y that provides file content indexing. Another similar job is that when the user downloads a (machine generated) file B from the system X, the file is also stored in the system Y for auditing. Both the system X and the system Y are RESTful Web Service. The file content transmitting through HTTP between the system X and the system Y is Base64 encoded as text in a JSON object. And the enocder and decoder I used is Apache Commons Codec.

Since Java 8 includes the Base64 encoder and decoder, it's time to write some codes with the encoder and decoder. However, using the IOUils.copy(InputStream, OutputStream) method in Apache Commons IO to handle reading something from input and putting the data into output is my personal habit. If the Apache Commons IO is not used, the helper method in Code List 1 is usefaul - writing while loop to copy data from the input to output is not needed any more.

Code List 1 - Copy data from the input stream to the output stream
public static void copy(InputStream input, OutputStream output) throws IOException {
    byte[] buffer = new byte[4096];
    int length = 0;
    while((length = input.read(buffer)) != -1) {
        output.write(buffer, 0, length);
    }
}

With the helper method, the first program uses Base64 encoder to enocde the data from the InputStream and then write the encoded data into the OutputStream. In Code List 2, the encode(InputStream, OutputStream, Base64.Encoder) method accepts three parameters. The first is the data source and the second is the encoded data destination. These two are easy to understand, but how about the third parameter? Why do I have to specify the encoder? Are there different kinds of Base64 encoders? Yes! Base64 has variants in different protocols. Java 8 provides three kinds of Base64 encoders/decoders. The first kind is the basic (using Base64.getEncoder() and Base64.getDecoder() to get the basic encoder and decoder, respectively) which only uses 0-9, a-z, A-Z, +, /, and = alphabet to perform encoding, and the content is not line-separated. The second kind is the URL and Filename safe (using Base64.getUrlEncoder() and Base64.getUrlDecoder() to get the encoder and decoder). Since the symbols + and / have special usages in the URL, and / is illegal to use as filename in many filesystems, the encoder uses - (minus) to replace + and uses _ (underline) to replace /. The third kind is MIME (using Base64.getMimeEncoder() and Base64.getMimeDecoder() to get the encoder and decoder, respectively) which uses the same alphabet as the basic, but adding \r\n after every 76 characters for line-separation. Assume that the basic is commonly used, a method without the third parameter like Code List 2 can be provided for convenient.

Code List 2 - Encode the data from the input stream with Base64 encoder
public static void encode(InputStream input, OutputStream output, Base64.Encoder encoder) {
    try(OutputStream encodedOutput = encoder.wrap(output)) {
        copy(input, encodedOutput);
    } catch(IOException e) {
        e.printStackTrace();
    }
}

public static void encode(InputStream input, OutputStream output) {
    encode(input, output, Base64.getEncoder());
}

After encoding, a decoder can be used to decode the data. In Code List 3, the decode(InputStream, OutputStream, Base64.Decoder) method also accepts three parameters: the first is the encoded data source, the second is the decoded data destination, and the third is the decoder. The three kinds of encoders/decoders mentioned above can not be used hybridly. That is the content encoded with the basic encoder can only be decoded with the basic decoder. The same, a method without the third parameter can be provided for convenient, and in the the following examples, the convenient version with the basic encoder/decoder is not listed for space saving.

Code List 3 - Decode the data from the input stream with Base64 decoder
public static void decode(InputStream input, OutputStream output, Base64.Decoder decoder) {
    try(InputStream decodedInput = decoder.wrap(input)) {
        copy(decodedInput, output);
    } catch(IOException e) {
        e.printStackTrace();
    }
}

public static void decode(InputStream input, OutputStream output) {
    decode(input, output, Base64.getDecoder());
}

With the method that encodes or decodes the content reading from the InputStream and then writes the encoded or decoded data into the OutputStream, variant can be provided for different usages. For example, in Code List 4, the method accepts two files as the input and output. In the method, the files are wrapped by FileInputStream and FileOutputStream and can be fed as the parameters of the methods in Code List 2 and Code List 3 to perform encoding or decoding operation.

Code List 4 - Encode/Decode the content the source file to the target file
public static void encode(File source, File target, Base64.Encoder encoder) {
    try(InputStream input = new FileInputStream(source);
        OutputStream output = new FileOutputStream(target)) {
        encode(input, output, encoder);
    } catch(IOException e) {
        e.printStackTrace();
    }
}

public static void decode(File source, File target, Base64.Decoder decoder) {
    try(InputStream input = new FileInputStream(source);
        OutputStream output = new FileOutputStream(target)) {
        decode(input, output, decoder);
    } catch(IOException e) {
        e.printStackTrace();
    }
}

Well, the encoded or decoded content is not always written into a file. In the job experience mentioned above, the file content is encoded as a part of the JSON object. For that case, the method in Code List 5 is useful - encoding the file content as a string. The method uses ByteArrayOutputStream as the buffer to store the encoded or decoded bytes, and then call toString(String) to output the content as a string.

Code List 5 - Encode/Deocde the file content as a string
public static String encode(File source, Base64.Encoder encoder) {
    String result = null;
    try(InputStream input = new FileInputStream(source);
        ByteArrayOutputStream output = new ByteArrayOutputStream()) {
        encode(input, output, encoder);
        result = output.toString("UTF-8");
    } catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

public static String decode(File source, Base64.Decoder decoder) {
    String result = null;
    try(InputStream input = new FileInputStream(source);
        ByteArrayOutputStream output = new ByteArrayOutputStream()) {
        decode(input, output, decoder);
        result = output.toString("UTF-8");
    } catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

The same, the data source may not be a file - a string is also possible. For the case, the method in Code List 6 can be used. The method uses getBytes() to obtain the string content as a byte array and wraps the byte array with ByteArrayInputStream as the input stream, and then feeds the input stream and the output stream for encoding or decoding operation.

Code List 6 - Encode/Decode the string
public static String encode(String source, Base64.Encoder encoder) {
        String result = null;
        try(InputStream input = new ByteArrayInputStream(source.getBytes());
            ByteArrayOutputStream output = new ByteArrayOutputStream()) {
            encode(input, output, encoder);
            result = output.toString("UTF-8");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

public static String decode(String source, Base64.Decoder decoder) {
    String result = null;
    try(InputStream input = new ByteArrayInputStream(source.getBytes());
        ByteArrayOutputStream output = new ByteArrayOutputStream()) {
        decode(input, output, decoder);
        result = output.toString("UTF-8");
    } catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

Java 8 includes the Base64 encoder and decoder to provide convenience. However, the third-party libraries like Apache Commons Codec usually provide many kinds of encoders and decoders more than Base64 - that is much more convenient. But if your application only needs Base64 encoder and decoder, in the Java 8 environment, the third-party library for Base64 encoder and decoder is not required. Well, only in the Java 8 environment, therefore, using the built-in Base64 encoder and decoder becomes a design trade-off.

Puzzle: try to decode the file (tip: the file is a PNG picture.)

 
over 4 years ago

這次看一個比較小的東西,就是Java 8終於將Base64編解碼器內建到java.util套件中了。Base64雖然會增加實際傳輸資料的長度,但在只用文字的網路協定中傳輸binary資料時常常用到(例如:MIME email),所以一直到Java 8才內建確實讓人意外,在Java 8之前,需要Base64編解碼器,都需要透過第三方函式庫,例如Apache Commons Codec

還記得之前某個工作內容是當使用者透過瀏覽器上傳A檔案到X系統時,因某些原故,X系統不提供檔案儲放的功能,實際上是將該檔案A放到另一個有提供檔案內容檢索的Y系統;另一個類似的情況是,當使用者透過瀏覽器在X系統下載B檔案(內容是機器生成的)時,該檔案B會放到Y系統的另一個資料夾中備查。X系統與Y系統都是標準的RESTful Web Service,當時,X和Y系統間檔案內容就是先以Base64編碼後,以文字的方式夾帶在JSON中透過HTTP傳輸。當時用的Base64編解碼器正是Apache Commons Codec。

既然Java 8內建Base64編解碼器,那就來寫點程式試用看看吧!不過個人的習慣,對於從輸入讀取資料放到輸出這種事,喜歡使用Apache Commons IOIOUils.copy(InputStream, OutputStream)函式,不過如果沒有使用Apache Commons IO,也可以自己寫一個(如Code List 1),之後就可以不用再寫while loop做資料讀取複製的程式了。

Code List 1 - Copy data from the input stream to the output stream
public static void copy(InputStream input, OutputStream output) throws IOException {
    byte[] buffer = new byte[4096];
    int length = 0;
    while((length = input.read(buffer)) != -1) {
        output.write(buffer, 0, length);
    }
}

有了輔助函式後,就先使用編碼器,將一個InputStream的資料用Base64編碼器編碼後放到指定的OutputStream,Code List 2中,encode(InputStream, OutputStream, Base64.Encoder)函式接受三個參數,第一個是代表資料的來源,第二個是代表編碼後資料的目的地,這兩個蠻容易理解的,那第三個呢?為什麼還要指定編碼器?難道Base64編碼有不同種類嗎?是的,Base64在不同通訊協定中有些變形,所以Java 8提供三種Base64的邊解碼器,第一種是基本版(分別用Base64.getEncoder()Base64.getDecoder()取得邊解碼器),只用0-9a-zA-Z+/=字元編碼且內容不換行;第二種針對網址和檔名修改的版本(用Base64.getUrlEncoder()Base64.getUrlDecoder()取得邊解碼器),由於+/符號在網址中有特殊用途,某些檔案系統也不允許/作為檔名,所以第二種用-(減號)取代+,用_(底線)取代/;第三種針對MIME調整的版本(用Base64.getMimeEncoder()Base64.getMimeDecoder()取得邊解碼器),使用的編碼字元和第一種一樣,但每輸出76個字元會加上一組\r\n換行。假設第一種是比較常用的情況,是可以如Code List 2那樣寫一個無第三參數的版本方便使用。

Code List 2 - Encode the data from the input stream with Base64 encoder
public static void encode(InputStream input, OutputStream output, Base64.Encoder encoder) {
    try(OutputStream encodedOutput = encoder.wrap(output)) {
        copy(input, encodedOutput);
    } catch(IOException e) {
        e.printStackTrace();
    }
}

public static void encode(InputStream input, OutputStream output) {
    encode(input, output, Base64.getEncoder());
}

編碼後就可以用解碼器進行解碼的動作,Code List 3中decode(InputStream, OutputStream, Base64.Decoder)同樣接受三個參數,第一個參數是待解碼的資料來源,第二個是解碼後的資料目的地,第三個是解碼器。剛提到的三種不同邊解碼器是無法混用的,所以使用第一種編碼器編碼的內容一定要用第一種解碼器來進行解碼。同樣,也可以提共一個無第三參數的版本方便使用,接下來的範例程式都可以用同樣的方法提供一個預先使用第一種邊解碼器的版本。

Code List 3 - Decode the data from the input stream with Base64 decoder
public static void decode(InputStream input, OutputStream output, Base64.Decoder decoder) {
    try(InputStream decodedInput = decoder.wrap(input)) {
        copy(decodedInput, output);
    } catch(IOException e) {
        e.printStackTrace();
    }
}

public static void decode(InputStream input, OutputStream output) {
    decode(input, output, Base64.getDecoder());
}

有了從InputStream讀取資料進行編碼或解碼動作後放到OutputStream的函式後,就可以來點不一樣的變形了,例如像Code List 4,可以指定輸入的檔案和輸出的檔案,利用FileInputStreamFileOutputStream將檔案包裝成串流來使用,這時就可以使用Code List 2和Code List 3提供的函示來進行編解碼的動作。

Code List 4 - Encode/Decode the content the source file to the target file
public static void encode(File source, File target, Base64.Encoder encoder) {
    try(InputStream input = new FileInputStream(source);
        OutputStream output = new FileOutputStream(target)) {
        encode(input, output, encoder);
    } catch(IOException e) {
        e.printStackTrace();
    }
}

public static void decode(File source, File target, Base64.Decoder decoder) {
    try(InputStream input = new FileInputStream(source);
        OutputStream output = new FileOutputStream(target)) {
        decode(input, output, decoder);
    } catch(IOException e) {
        e.printStackTrace();
    }
}

當然,很多時候編解碼後的內容並不是要放到檔案中,以剛剛提到的例子,檔案內容會先被編碼,然後當成JSON的一部分,這時會希望有類似Code List 5的程式,將編解碼後的內容當成字串,這時可以用ByteArrayOutputStream暫時存放邊解碼後的內容,然後使用toString(String)將結果以字串輸出。

Code List 5 - Encode/Deocde the file content as a string
public static String encode(File source, Base64.Encoder encoder) {
    String result = null;
    try(InputStream input = new FileInputStream(source);
        ByteArrayOutputStream output = new ByteArrayOutputStream()) {
        encode(input, output, encoder);
        result = output.toString("UTF-8");
    } catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

public static String decode(File source, Base64.Decoder decoder) {
    String result = null;
    try(InputStream input = new FileInputStream(source);
        ByteArrayOutputStream output = new ByteArrayOutputStream()) {
        decode(input, output, decoder);
        result = output.toString("UTF-8");
    } catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

同樣,需要被編解碼的資料來源不一定是從檔案來,可能就是一個字串,這時Code List 6的函式就幫得上忙了,先用getBytes()取得字串的位元組陣列,接著用ByteArrayInputStream將位元組陣列包裝成輸入串流,然後用同樣的方法,對輸入的字串進行編解碼的動作。

Code List 6 - Encode/Decode the string
public static String encode(String source, Base64.Encoder encoder) {
        String result = null;
        try(InputStream input = new ByteArrayInputStream(source.getBytes());
            ByteArrayOutputStream output = new ByteArrayOutputStream()) {
            encode(input, output, encoder);
            result = output.toString("UTF-8");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

public static String decode(String source, Base64.Decoder decoder) {
    String result = null;
    try(InputStream input = new ByteArrayInputStream(source.getBytes());
        ByteArrayOutputStream output = new ByteArrayOutputStream()) {
        decode(input, output, decoder);
        result = output.toString("UTF-8");
    } catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

Java 8有了Base64編解碼器,方便不少,不過Apache Commons Codec提供更多常用的編解碼器,其實是更方便的,但如果你的應用程式中只需要Base64編解碼器,在有Java 8的環境中確實不需要將Apache Commons Codec和專案一起打包,對~前提是要有Java 8的環境,變成用不用內建的Base64編解碼器又是一種抉擇了。

猜謎活動:試著解開這個檔案的內容(提示是這檔案是一個PNG圖檔)。

 
over 4 years ago

Today, the topic is not about the Lambda expression -- is about the other change to the language in Java 8: default methods. In the past, the Java interface can only declare methods, but can not provide implementation (or called define methods). Therefore, in most design, static utility methods are defined in a class. Basically, that is not a big problem. However, someone may think about that should I define a private default constructor for the class?

In addition, Java does not support multiple inheritance, but supports multiple interface implementations. Thus, in the Java library, there is a commonly seen pattern: an interface that declares the required methods, an abstract class that provides the common implementation, and a default class that provides a completed implementation. For example, TableModel declares the minimum method set required by JTable, AbstractTableModel provides some common implementation (e.g., the management of listeners), and DefaultTableModel provides a completed implementation. This pattern may give rise to a problem: if class A has inherited class B, and wants to use (inherit) the abstract class of the interface C to reuse the implementation of C. However, Java does not support multiple inheritance, the only way is that A implements C by itself, but, to a certain extent, that will produce duplicated code.

During the design of Comic Surfer, in order to keep the binary compatibility between the main program and the plug-ins, I used the pattern mentioned above. In the public plug-in SDK, both interfaces and abstract classes are provided. However, in the public developer guide, I encourage the developers to inherit abstract classes to develop the plug-in. In that design, when I want to add methods into existing interfaces, I also add default implementation of new added methods into the abstract classes. Therefore, the plug-in can still run with the new version of Comic Surfer without any revising -- JVM will not complain that the class does not provide the required implementation of some methods.

For the above issues, the default methods can provide a pretty good solution. First, it is no need to define the utility methods in a class anymore. As shown in Code List 1, the implementation of static methods can be defined in an interface directly.

Code List 1 - Static utility methods in an interface
package java8.defaults;

import java.util.Collection;
import java.util.function.Predicate;

public interface CollectionUtils {

    public static <T> T find(Collection<T> collection, Predicate<T> predicate) {
        for(T item : collection) {
            if(predicate.test(item)) {
                return item;
            }
        }
        return null;
    }
}

In addition, to a certain extent, if no member data is inside an abstract class, the role of abstract classes can be replaced. Assume that an interface is required for temperature sensors. The TemperatureSensor interface can be desinged like Code List 2: two default methods getTemperatureInCelsius() and getTemperatureInFahrenheit() are provided to convert the Kelvin scale to Celsius scale and Fahrenheit scale, respectively. I knew that the design example is not good enough because the conversion can be extracted into independent classes, however, the simplification is appropriate to show the usage of default methods. Since the implementation has be defined in the interface, the abstract class AbstractTemperatureSensor as shown in Code List 3 is not needed anymore.

Code List 2 - The default methods in an interface
package java8.defaults;

public interface TemperatureSensor {

    static final double KELVIN_TO_FAHRENHEIT_FACTOR = 1.8;
    static final double KELVIN_TO_FAHRENHEIT_OFFSET = 459.67;
    static final double KELVIN_TO_CELSIUS_OFFSET = 273.15;

    public double getTemperatureInKelvin();

    default public double getTemperatureInCelsius() {
        return getTemperatureInKelvin() - KELVIN_TO_CELSIUS_OFFSET;
    }

    default public double getTemperatureInFahrenheit() {
        return getTemperatureInKelvin() * KELVIN_TO_FAHRENHEIT_FACTOR - KELVIN_TO_FAHRENHEIT_OFFSET;
    }
}
Code List 3 - The replaced abstract class
package java8.defaults;

public abstract class AbstractTemperatureSensor implements TemperatureSensor {

    private static final double KELVIN_TO_FAHRENHEIT_FACTOR = 1.8;
    private static final double KELVIN_TO_FAHRENHEIT_OFFSET = 459.67;
    private static final double KELVIN_TO_CELSIUS_OFFSET = 273.15;

    @Override
    public double getTemperatureInCelsius() {
        return getTemperatureInKelvin() - KELVIN_TO_CELSIUS_OFFSET;
    }

    @Override
    public double getTemperatureInFahrenheit() {
        return getTemperatureInKelvin() * KELVIN_TO_FAHRENHEIT_FACTOR - KELVIN_TO_FAHRENHEIT_OFFSET;
    }
}

No need of the abstract class provides some flexibility in design. For example, trying to write an adapter for a real temperature sensor device (AbstractDeviceControl), if default methods are not supported, due to the single inheritance, the developer needs consider that let the adapter inherit whether AbstractDeviceControl or AbstractTemperatureSensor? Of course, composition over inheritance, but, sometimes, developers need to write more codes. Now, just like Code List 4, the adapter can obtain the implementation from the interface, and the only one chance to inherit a class is left to other designs.

Code List 4 - Get the default implementation from interfaces
package java8.defaults;

public class TemperatureSensorDevice extends AbstractDeviceControl implements TemperatureSensor {

    private static final int COMMAND_CONTROL_PORT = 0x11;
    private static final int TEMPERATURE_OUTPUT_PORT = 0x10;
    private static final int TEMPERATURE_COMMAND = 0x0101;

    @Override
    public double getTemperatureInKelvin() {
        openCommandControlPort(COMMAND_CONTROL_PORT);
        setCommandOutputPort(TEMPERATURE_OUTPUT_PORT);
        performCommand(TEMPERATURE_COMMAND);
        return getValueFromPort(TEMPERATURE_OUTPUT_PORT);
    }
}

Then, while adding new methods into an existing interface, the default methods in the interface can provide the backward compatibility. For example, in Code List 5, a conversion to a new scale, called Rankine scale, is added into the TemperatureSensor interface with the default implementation. And, TemperatureSensorDevice can be used directly without any modification.

Code List 5 - Add a new method into TemperatureSensor with default implementation
package java8.defaults;

public interface TemperatureSensor {

    static final double KELVIN_TO_FAHRENHEIT_FACTOR = 1.8;
    static final double KELVIN_TO_FAHRENHEIT_OFFSET = 459.67;
    static final double KELVIN_TO_CELSIUS_OFFSET = 273.15;

    public double getTemperatureInKelvin();

    default public double getTemperatureInCelsius() {
        return getTemperatureInKelvin() - KELVIN_TO_CELSIUS_OFFSET;
    }

    default public double getTemperatureInFahrenheit() {
        return getTemperatureInKelvin() * KELVIN_TO_FAHRENHEIT_FACTOR - KELVIN_TO_FAHRENHEIT_OFFSET;
    }

    default public double getTemperatureInRankine() {
        return getTemperatureInKelvin() * KELVIN_TO_FAHRENHEIT_FACTOR;
    }
}

An interface can be extended to a new interface. For example, in some countries, people are used to Fahrenheit scale and many devices only provide the value in Fahrenheit scale. For that, like Code List 6, a new interface FahrenheitTemperatureSensor extends the TemperatureSensor interface. In the extension, the abstract modifier is used to redeclare the getTemperatureInFahrenheit() method that has default implementation as abstract method. A default method can also be overridden, e.g., getTemperatureInCelsius() is overridden to convert the Celsius scale from the Fahrenheit scale directly, saving the conversion between Kelvin scale and Fahrenheit scale.

Code List 6 - Redeclare getTemperatureInFahrenheit() as an abstract method
package java8.defaults;

public interface FahrenheitTemperatureSensor extends TemperatureSensor {

    static final double FAHRENHEIT_TO_KELVIN_FACTOR = 5.0 / 9.0;
    static final double FAHRENHEIT_TO_CELSIUS_OFFSET = 32;

    abstract public double getTemperatureInFahrenheit();

    default public double getTemperatureInKelvin() {
        return (getTemperatureInFahrenheit() + KELVIN_TO_FAHRENHEIT_OFFSET) * FAHRENHEIT_TO_KELVIN_FACTOR; 
    }

    default public double getTemperatureInCelsius() {
        return (getTemperatureInFahrenheit() - FAHRENHEIT_TO_CELSIUS_OFFSET) * FAHRENHEIT_TO_KELVIN_FACTOR;
    }
}

In fact, many developers discussed a lot about default methods because with both default methods and multiple interface implementations can provide some kind of multiple inheritance like C++ -- except that the member data are not inherited. Good or bad? It depends on how to use. I think default methods helpful. If default methods are not only available in Java 8, I really would like to revise the plug-in interfaces and utility classes with default methods in my Comic Surfer.

 
over 4 years ago

今天離開Lambda相關的議題,看一下Java 8另一個和語言相關的改變:default methods。過去,Java的介面只能宣告函式卻不能提供實作(或是說是定義函式,定義函式和宣告函式的差別就是有沒有實作),因此大多數的library設計都將工具函式(static utility methods)放在類別中,這基本上沒什麼大問題,但是對有些潔癖的人來說,就會考慮要不要把該類別的建構子設為private?

另外,Java不支援多重繼承,但能實作多個介面,所以在Java的函式庫裡常會看到一種設計慣例:介面宣告需要的函式,透過抽象類別提供共用的預設實作,最後由預設類別提供完整的實作,例如:TableModel宣告JTable所需要的最小函式集合,AbstractTableModel提供部分共用的實作(listeners的管理),最後DefaultTableModel提供一個完整的實作。這樣的慣例引起另一個問題:但是當A類別繼承了B類別,但又想使用C介面的抽象類別實作就沒辦法了(因為不能多重繼承),A類別只能自己實作C介面,某種程度上算是一種duplicated code。

自己在設計Comic Surfer的過程中,為了讓既有的外掛和主程式能維持binary compatibility,也採用上述的慣例,在公開的外掛SDK裡,同時放了介面和抽象類別,不過在開發文件上鼓勵開發者以繼承抽象類別的方式開發外掛,這樣的設計是當我想在介面中新增函式的宣告,同時在抽象類別中為新增的函式提供預設的實作,如此,即使外掛沒有用新的SDK進行修改,還是能在新版的Comic Surfer中掛載使用,而不會讓JVM抱怨說某個類別沒有提供完整的實作(不相容)。

上述的問題,在這次的default methods得到了一個不錯的解法。首先,工具函式不需要再放在類別中了,如Code List 1所示,靜態函式的實作能直接放在介面中。

Code List 1 - Static utility methods in an interface
package java8.defaults;

import java.util.Collection;
import java.util.function.Predicate;

public interface CollectionUtils {

    public static <T> T find(Collection<T> collection, Predicate<T> predicate) {
        for(T item : collection) {
            if(predicate.test(item)) {
                return item;
            }
        }
        return null;
    }
}

接著,抽象類別某種程度上可以被取代(如果抽象類別帶有成員變數就沒辦法),假設要替溫度感測器設計一個介面,可以像Code List 2的TemperatureSensor介面,並提供二個default methods:getTemperatureInCelsius()getTemperatureInFahrenheit()分別將絕對溫度轉成常用的攝氏溫度和華氏溫度(這個例子是一個簡化的示意。不算是一個好設計,因為這轉換應該算是能抽離到外部變成一個獨立類別)。這時過去需要提供類似Code List 3中的抽象類別AbstractTemperatureSensor就完全不需要了,因為實作已經放在介面裡了。

Code List 2 - The default methods in an interface
package java8.defaults;

public interface TemperatureSensor {

    static final double KELVIN_TO_FAHRENHEIT_FACTOR = 1.8;
    static final double KELVIN_TO_FAHRENHEIT_OFFSET = 459.67;
    static final double KELVIN_TO_CELSIUS_OFFSET = 273.15;

    public double getTemperatureInKelvin();

    default public double getTemperatureInCelsius() {
        return getTemperatureInKelvin() - KELVIN_TO_CELSIUS_OFFSET;
    }

    default public double getTemperatureInFahrenheit() {
        return getTemperatureInKelvin() * KELVIN_TO_FAHRENHEIT_FACTOR - KELVIN_TO_FAHRENHEIT_OFFSET;
    }
}
Code List 3 - The replaced abstract class
package java8.defaults;

public abstract class AbstractTemperatureSensor implements TemperatureSensor {

    private static final double KELVIN_TO_FAHRENHEIT_FACTOR = 1.8;
    private static final double KELVIN_TO_FAHRENHEIT_OFFSET = 459.67;
    private static final double KELVIN_TO_CELSIUS_OFFSET = 273.15;

    @Override
    public double getTemperatureInCelsius() {
        return getTemperatureInKelvin() - KELVIN_TO_CELSIUS_OFFSET;
    }

    @Override
    public double getTemperatureInFahrenheit() {
        return getTemperatureInKelvin() * KELVIN_TO_FAHRENHEIT_FACTOR - KELVIN_TO_FAHRENHEIT_OFFSET;
    }
}

不需要抽象類別,提供了一個設計上的彈性,例如要替一個溫度感測裝置控制器(AbstractDeviceControl)寫Adapter時,在沒有default methods前,因為單一繼承,要思考是讓Adapter繼承AbstractDeviceControl比較好,還是繼承AbstractTemperatureSensor比較好?當然,用composition解決更好,只是有時候會多寫些程式。現在可以像Code List 4用透過實作介面的方式取得預設實作,然後將唯一的繼承機會讓給其他可能需要的設計。

Code List 4 - Get the default implementation from interfaces
package java8.defaults;

public class TemperatureSensorDevice extends AbstractDeviceControl implements TemperatureSensor {

    private static final int COMMAND_CONTROL_PORT = 0x11;
    private static final int TEMPERATURE_OUTPUT_PORT = 0x10;
    private static final int TEMPERATURE_COMMAND = 0x0101;

    @Override
    public double getTemperatureInKelvin() {
        openCommandControlPort(COMMAND_CONTROL_PORT);
        setCommandOutputPort(TEMPERATURE_OUTPUT_PORT);
        performCommand(TEMPERATURE_COMMAND);
        return getValueFromPort(TEMPERATURE_OUTPUT_PORT);
    }
}

然後,如果要替介面新增函式,也可以透過直接在介面中提供預設實作,提供向下相容性。例如Code List 5新增蘭金溫度表示法(Rankine scale)到TemperatureSensor中,並提供預設的實作,這時TemperatureSensorDevice不需做任何修改就能夠使用。

Code List 5 - Add a new method into TemperatureSensor with default implementation
package java8.defaults;

public interface TemperatureSensor {

    static final double KELVIN_TO_FAHRENHEIT_FACTOR = 1.8;
    static final double KELVIN_TO_FAHRENHEIT_OFFSET = 459.67;
    static final double KELVIN_TO_CELSIUS_OFFSET = 273.15;

    public double getTemperatureInKelvin();

    default public double getTemperatureInCelsius() {
        return getTemperatureInKelvin() - KELVIN_TO_CELSIUS_OFFSET;
    }

    default public double getTemperatureInFahrenheit() {
        return getTemperatureInKelvin() * KELVIN_TO_FAHRENHEIT_FACTOR - KELVIN_TO_FAHRENHEIT_OFFSET;
    }

    default public double getTemperatureInRankine() {
        return getTemperatureInKelvin() * KELVIN_TO_FAHRENHEIT_FACTOR;
    }
}

既然是介面,當然能用擴充(extend)的方式產生新介面,例如,某些國家習慣用華氏溫度,所以很多感測器都只提供華氏溫度,這時可像Code List 6,擴充TemperatureSensor介面產生新介面FahrenheitTemperatureSensor,在擴充的時候,可以用abstract關鍵字將一個有預設實作的getTemperatureInFahrenheit()重新宣告為抽象函式,或是複寫既有的實作,例如複寫getTemperatureInCelsius()變成直接取華氏溫度轉攝氏溫度,不用多轉一次絕對溫度然後再轉攝氏溫度。

Code List 6 - Redeclare getTemperatureInFahrenheit() as an abstract method
package java8.defaults;

public interface FahrenheitTemperatureSensor extends TemperatureSensor {

    static final double FAHRENHEIT_TO_KELVIN_FACTOR = 5.0 / 9.0;
    static final double FAHRENHEIT_TO_CELSIUS_OFFSET = 32;

    abstract public double getTemperatureInFahrenheit();

    default public double getTemperatureInKelvin() {
        return (getTemperatureInFahrenheit() + KELVIN_TO_FAHRENHEIT_OFFSET) * FAHRENHEIT_TO_KELVIN_FACTOR; 
    }

    default public double getTemperatureInCelsius() {
        return (getTemperatureInFahrenheit() - FAHRENHEIT_TO_CELSIUS_OFFSET) * FAHRENHEIT_TO_KELVIN_FACTOR;
    }
}

其實,default methods似乎也引起不小的討論,因為default methods加上可以實作多個介面,已經有點像C++的多重繼承了,只差在沒辦法繼承成員變數而已,是好是壞就看怎麼使用了。我個人覺得還蠻方便的,如果不是因為default methods要到Java 8才有,我倒是考慮將Comic Surfer的外掛介面和工具函式類別用default methods改寫。

 
over 4 years ago

Today, let's discuss something not so serious. After I read "Beyond Java," many years later, I rethink about the Java language. I think that Java puts too much emphasis on the library (J2EE has become a big monster), and ignores the language itself (for example, the auto-closable resource in try-catch in J2SE 7 and the Lambda expression in J2SE 8 are little big-changes to the language itself after the generic and autoboxing in J2SE 5). Java SE 8 has released, and not more than one month (2014/3/18 -> 2014/4/15), Java SE 8 Update 5 also released (the version number jumps fast after Oracle takes charge of Java development). The next version, Java SE 9, is prepared to launch. However, after learned different languages, I hope some language features (not necessarily functional) can be added into the future Java.

The first feature I hope to be added is property. In fact, many languages provide the similar language feature, e.g., Objective C and C#. The property can be accessed like variables. For example, in Code List 1, access the publications of a person by using for(Publication* publication in person.publications), or in Code List 2, change the height of a page by using page.Height = 400. It is not the same as declaring public member data, because a property is consisted of a setter and a getter with access control. Therefore, in Code List 2, the change to the height will notify all handlers (observer pattern) through the setter. And some languages provide default implementation of getters/setters, or mechanism to generate getters/setters automatically, e.g, @synthesize firstName;. Although getters/setters can also be generated automatically with Lombok annotations, that is not a part of the Java language. In addition, I don't like use annotation to generate getters/setters (well, this is a personal preferences).

Code List 1 - The property in Objective C
// In Person.h
@interface Person : NSObject

@property (nonatomic, readonly) NSInteger age;
@property (nonatomic, readonly) NSArray* publications;
@property (nonatomic) NSString* firstName;
@property (nonatomic) NSString* lastName;

@end

// In Person.m
@implementation Person {
    NSMutableArray* _publications;
}

@synthesize firstName;
@synthesize lastName;

- (NSInteger)age {
    NSDate* date = [NSDate date];
    NSCalendar* calendar = [NSCalendar currentCalendar];
    NSDateComponents* components = [calendar components:NSYearCalendarUnit fromDate:self.birthday toDate:now  options:0];
    return [ageComponents year];
}

- (NSArray*)publications {
    return _publications;
}
@end

// In somewhere alse
Person* person = [controller getPerson];
for(Publication* publication in person.publications) {
    // do something
}
Code List 2 - The property in C#
// In Page.cs
public class Page : Drawable {

    /// <summary>
 /// Gets or sets the height of the page
 /// </summary>
 public int Height {
        get { return _height; }
        set {
            _height = value;
            NotifyContentUpdatedEventHandlers();
        }
    }
}

// In somewhere alse
Page page = createEmptyPage();
page.Height = 400;

The second feature is literal data structure declaration. Both JavaScript and Objective C provide this feature, Sometimes, when processing datta, a class with getters/setters is required to process a simple data object -- that is boring. For example, in Code List 3, a company data object is created easily. Yes, in fact, Code List 3 is a feature that Objective C borrowed from JavaScript Object Notation (JSON) as shown in Code List 4. Just use a symbol @ to declare literal data structure, and Java can use the same way! Well, someone can explain why the built-in JSON API only available in J2EE -- client does not need to parse JSON? That is somewhere strange.

Code List 3 - The literal data structure declaration
id company = @{
    @"name": @"Far Far AwayCompany",
    @"address": @"I don't want to know",
    @"foundedOn": @1999,
    @"employees": @[ @"Bill", @"Steve", @"John" ]
};
Code List 4 - JavaScript Object Notation
var company = {
    "name": "Far Far AwayCompany",
    "address": "I don't want to know",
    "foundedOn": 1999,
    "employees": [ "Bill", "Steve", "John" ]
};

The third feature is category. That is my favor feature of Objective C. Category is a way to glue new functions to an existing class, but it is not inheritance because the type is not changed. It is not the same as the partial keyword in C#. The category is very useful to design complicated project. The functionalities of a class can be categoried into different files. For example, the conversion between a POJO (Plain Old Java Object) object and JSON string is the responsibility of the POJO object itself, or someone others?

I would choose the prior one when I was new to the OO language, but after I saw many desings, I prefer the latter. Since an object can be outputted to different formats (JSON or XML), when a new format is required to output, the modification to the class source code againsts the open close principle. Without the source code, modification is impossible. Inheritance can add new functions, but the type is changed. With category, the implementation of the new format output can be glued to the existing class. As shown in Code List 5, the implementation of Person class in Person.h only provides the logics required by the domain model. And the implementation of the JSON conversion is put in the JSON category. If JSON conversion is needed, just import Person+JSON.h to get the implementation; otherwise, only import Person.h, the implementation of the JSON conversion is invisible. The category can glue new functionalities without the source code of the glued class -- I often use the category to add new functions into NSString.

Code List 5 - Category
// In Person.h
@interface Person : NSObject

// domain model logic

@end

// In Person+JSON.h
@interface Person (JSON)

+ (instancetype)fromJson:(id)json;

- (void)updateWithJson:(id)json;

- (id)toJson;

@end

// In somewhere alse
#import "Person+JSON.h"

Person* person = [Person fromJson:json];

The fourth feature is extension. This is also a feature of Objective C, and I hope this featureadded into Java to improve the testability. Although the modifiers @public, @protected, and @private are available in Objective C, they are used on member data, not member methods. Objective C does not require that all member methods should be declared in the header file (.h). Therefore, in most case, the methods written in the implementatio file (.m) are considered as the private methods. Sometimes, if private methods can become public methods, to write test case will be more easier. It is good chance to use the extension feature. As an example in Code List 6, public methods are declared in the Person.h, and the methods only public on testing are declared in Person_Private.h. Only the Person.h is public to everyone, and the Person_Private.h is not public. While testing, private methods can be seen by importing the Person_Private.h. (In fact, without additional header files, the extension can be used to open the private methods in test class directly.) Although Java does not separate the header file from the implementation file, providing the private interface implementation may be a similar solution.

Code List 6 - Extension
// In Person.h
@interface Person : NSObject

// public domain model logic

@end

// In Person_Private.h
#import "Person.h"

@interface Person()

- (void)privateMethod;

@end

// In Person.m
#import "Person_Private.h"

@implementation Person

// Implement the methods that declared in Person.h and Person_Private.h

@end

// In PersonTests.m
#import "Person_Private.h"

@implementation PersonTests

- (void)testPrivateMethod {
    // Can see and test -(void)privateMethod method declared in Person_Private.h in test mode.
}

@end

The fifth feature is the preprocessor that can be integrated with IDE. Many developers started to write programs with simple text editors (well, basic funtionalities like syntax highlighted are still offered in these editors), because many IDEs becomes slow and heavy. However, there are still many developers use IDE to write programs -- I ever tried to find an alternative of the slow Eclipse, but I continue to use it because 4.3 improved a lot on performance. The IDEs of Objective C and C# provides many good integration with languages. There are many good IDEs for Java, but Java does not provide features to support IDE. For example, I like to organize methods in a class baed on their functions, and in Objective C, I would like to use #param mark - UITableViewDelegate Methods to collect the methods for UITableViewDelegate together, and in C#, I can use a pair of #region Properties and #endregion to wrap all properties in the same block. And IDE can optimize the UI based on these preprocessors. In XCode, the navigation bar (Figure 1) will classify the methods based on the name marked in #param mark, and this will be easier to find a method.

Figure 1 - XCode provides integration with preprocessors

In recent years, new languages focus on the productivity and improve the readability (abstraction level). And most important is that they can develop the domain specific languages easily. That is not easy for Java. In fact, the features mentioned in the article are something like syntax sugar but they can improve the readability and abstraction level. Therefore, I think these features are useful in Java.