over 4 years ago

## 希望Java未來能新增的特性

Figure 1 - XCode provides integration with preprocessors

almost 5 years ago

## Quick Glance of Java 8 - Lazy Evaluation & Parallel Stream

In addition to the pipe design, I am interested in other two features of Stream: lazy evaluation and parallel stream. Lazy evaluation can be considered as the computation is delayed util it is actually needed. And the parallel stream can execute the entire pipe in parallel. The most important thing is that these two features are optimized for JVM and should be more efficient than our implementation.

When I see the lazy evaluation, I think it could be benefit to loading large files, e.g., reducing the memory consumption. However, I am not sure for that. Therefore, I design an experiment to confirm my thought. First, an interface FileSearchStrategy is created (Code List 1) and its method can accept a file (folder), a keyword, and a result collector (SearchResultCollector). Each implementation can use different methodologies to search the keyword in a file and put the result, a tuple <filename, line number, line content>, into the collector.

The File object in Java can be pointed to a file or a folder. Thus, AbstractSearchStrategy (Code List 2) provides an implementation for the method search(File, String, SearchResultCollector) of FileSearchStrategy to traverse each folder recursively. A hook method is declared for concrete classes to scan the content of a real file.

The infrastructure is completed. And the first strategy implementation DefaultSearchStrategy (Code List 3) uses the frequently-used algorithm on text files before having Stream API: scan line by line. The result of the strategy is also the baseline of the experiment.

Then, the implementation of AllLinesSearchStrategy uses the readAllLines(Path) method of Files class which coming with the NIO 2 (New I/O 2) package in Java 7 (Code List 4). In fact, the description in Java Doc says that it is convenient to read all lines in a single operation and not intended for reading in large files. Therefore, the result of the strategy should be the worst case in the experiment.

For convenient to the following parallel stream experiment, the implementation of StreamSearchStrategy puts the pipe combination into another method (Code List 5): scanStream(Stream, String, SearchResultCollector). And then uses the lines() method of the BufferedReader class to get the Stream as input. In order to obtain the line number, the intermediate operation in the pipe is map(Function) which transfers a string into an object consisting of a line number and a string (using KeywordSearchResult for simplification). And filter(Predicate) is used to filter out objects that do not match.

Since the line number is required, the scanStream(Stream, String, SearchResultCollector) method of the StreamSearchStrategy class uses map(Function) before filter(Predicate). If the line number can be ignored, as shown in Code List 6, the StreamSearchStrategyV2 class uses filter(Predicate) before map(Function), does it affect the result?

The size of the system memory grows quickly, and the operating system usually caches the file content. Therefore, reading the same file again is speeded up. However, the acceleration will be an impact factor. To avoid reading the file content from the cache, the experiment prepares three folders, in each, consisting of the same file structure and content listed in Table 1. For example, the sub-folder A has 10 files, 100k records in each file (size is 3.62 MB), and sub-folders B, C, etc are the same. The sub-folder G cloned a copy of the sub-folders A ~ F, and as a result, each folder has 120 files, totally sized 4.45 GB.

Table 1 - Test data

Sub Folder Records File Amount Size/File (MB)
A 100k 10 3.62
B 200k 10 7.24
C 400k 10 14.4
D 800k 10 28.9
E 1600k 10 57.9
F 3200k 10 115
G A + B + C + D + E + F

However, the success of the method depends on the size of the system memory. The experiment environment is listed in Table 2, and the test process is shown in Code List 7. Each strategy searches the keyword in the three prepared folders in order. When the second strategy is started to search the first folder, the content of the other two folders should be loaded into memory by the first strategy. The total size is 8.9 GB that is larger than the size of the system memory. Therefore, the content of the first folder is not in the cache. A MemoryUsageMonitor object monitors the memory usage of JVM periodically and records the peak value in the experiment,

Table 2 - Test environment

Hardware Specification
CPU Intel Core i5-2400
Memory 8GB
HDD Seagate ST3160815AS 160GB
OS Windows 7 SP1
JVM 1.8.0-b132

Well, it is time to show the result! The result is shown in Table 3. As expected, All Lines strategy uses the most memory (over 1GB) and took the longest time (20 seconds longer than other strategies). However, the differences between Default, Stream, and Stream v2 are not significant (about 3 seconds). The memory usage of the Stream v2 and Default strategies are almost the same, but the map(Function) seems to be a bad cost.

Table 3 - Test result of four strategies

Strategy Time (ms) Memory (MB)
Default 106788 42.8
Stream 109272 57.4
Stream v2 109402 41.7
All Lines 128749 1140.9

The AbstractSearchStrategy uses the tranditional foreach to traverse each file. Does the parallel stream benefit? or make worse? To understand that, the implementation of the search(File, String, SearchResultCollector) method of the AbstractSearchStrategy class is changed to Code List 8, and run the experiment again.

Suppose the parallel stream can speed up the search. However, from the result of Table 4, the parallel stream does not speed up the search, and makes wrose: huge memory consumption. I am surprised at that the memory usage of the Stream v2 strategy is more than that of the Stream strategy, and I don't known how to expain the phenomenon?

Table 4 - Test result of four strategies with parallel directories traversal

Strategy Time (ms) Memory (MB)
Default 109386 281.7
Stream 109230 397.1
Stream v2 108978 487.5
All Lines 122702 1441.9

From the experiment result, when I/O is involved, even using parallel stream, a pipe with Stream does not speed up or use less memory. Sometimes, it is slower. If the data is not in the files on hard disk, how about the effect on processing data in the memory? Therefore, the third experiment is designed. Suppose that 100k ~ 6400k records are kept in an ArrayList, and based on the parameter (Code List 9), use the parallelStream() or stream() method to obtain the input of scanStream(Stream, String, SearchResultCollector).

The result is listed in Table 5. Note that, in the 100k column, no matter run Stream strategy first or run Stream v2 strategy first, the first run strategy always get a bad result. The cause may be the cold start up of the program. Thus, the 100k column is ignored. Starting from 200k, both the Stream and Stream v2 strategies can be beneifted by parallelStream() to reduce the execution time a lost with. In the 6400k column, Stream v2 with parallel stream can save 136 ms. In general, the performance of the Stream v2 strategy is better than that of the Stream strategy.

Table 5 - The execution time (ms) with parallel stream

Strategy Parallel 100k* 200k 400k 800k 1600k 3200k 6400k
Stream Close 47 8 15 30 57 187 342
Stream Open 29 5 11 21 42 83 165
Stream v2 Close 12 7 12 24 47 93 192
Stream v2 Open 2 3 5 9 15 29 56

It is time to give a conclusion. First, to be benefited by the lazy evaluation, the optimization of the pipe design is required. Bad pipe design makes the performance worse. And I/O can not get lots of benefit from the lazy evaluation. The parallel stream can speed up the processing on some kinds of data sources. The I/O data source or the data source that to access may have race condition will not speed up by the parallel stream. If the data is in memory already or the data source that to access without lock, parallel stream can speed up much. However, the parallel stream also increases the memory usage, so the parallel stream should be used carefully.

ps. The source code is still under organization. When the organization is completed, the source code will be opened on GitHub.

almost 5 years ago

## Java 8 初探 - Lazy Evaluation & Parallel Stream

Table 1 - Test data

Sub Folder Records File Amount Size/File (MB)
A 100k 10 3.62
B 200k 10 7.24
C 400k 10 14.4
D 800k 10 28.9
E 1600k 10 57.9
F 3200k 10 115
G A + B + C + D + E + F

Table 2 - Test environment

Hardware Specification
CPU Intel Core i5-2400
Memory 8GB
HDD Seagate ST3160815AS 160GB
OS Windows 7 SP1
JVM 1.8.0-b132

Table 3 - Test result of four strategies

Strategy Time (ms) Memory (MB)
Default 106788 42.8
Stream 109272 57.4
Stream v2 109402 41.7
All Lines 128749 1140.9

Table 4 - Test result of four strategies with parallel directories traversal

Strategy Time (ms) Memory (MB)
Default 109386 281.7
Stream 109230 397.1
Stream v2 108978 487.5
All Lines 122702 1441.9

Table 5 - The execution time (ms) with parallel stream

Strategy Parallel 100k* 200k 400k 800k 1600k 3200k 6400k
Stream Close 47 8 15 30 57 187 342
Stream Open 29 5 11 21 42 83 165
Stream v2 Close 12 7 12 24 47 93 192
Stream v2 Open 2 3 5 9 15 29 56

ps. 測試程式碼還在整理中，若整理完會公開到GitHub上。

almost 5 years ago

## Quick Glance of Java 8 - Stream

Java Collection framework is a well-designed framework even without the Stream API. However, sometimes, writing a simple function, for example, finding an object in a collection based on some conditions, needs a for loop. Writing such a program is easy, but writing similar for-loop many times is boring. Before having the "for in" syntax surgar, the index calculation in a for loop is annoying. In addition, if the indices naming is not appropriate, debuging in a nested for loops is a terrible job. After using Apache Commons Collections, Apache Commons Collections has become the necessary library in my every project. Here is a simple example, to find an Person object in a List whose first name or last name matches the given value, the traditional way is like Code List 1 -- writing a for loop to check every person's first name and last name.

How about using Apache Commons Collections? The program is lised in Code List 2.a, and basically, a for-loop is not needed. Just call the find method with an object that implements the Predicate interface. Code List 2.b shows the implementation of the object. Only the evaluate method is required to implement -- return true if the given object matches the condition. That's all. What!? The lines of code become more than that of Code List 1. Yes. It does, but the PersonNamePredicate is reusable and easy to test. In the CollectionUtils class, there are 12 methods that use Predicate object to filter, select, or count objects in a collection. Therefore, I think it is worth writing the class.

Well, the topic of this article is the new Stream API in Java 8. So how to find an object with the Stream API? The answer is shown in Code List 3. The lines of code are not reduced much. However, in comparison with Code List 1, Code List 3 can be interpreted as filter the objects out based on a condition and return the first one if it exists; otherwise return null and the detail of the loop is ignored. So in semantic or readability, does this way improve the level of abstraction?

Is it possible to find an object like Code List 2.a, but with Stream API? Yes, it is possible. First, write a helper class StreamUtils like Code List 4.a which provides a method find(Collection, Predicate). Second, revise the PersonNamePredicate as Code List 4.b, and then use just one line of code to find an object like Code List 4.c. Of course, if you do not want PersonNamePredicate to support both Apache Commons Collections and Java Stream API, the test method of java.util.Predicate is the only method required to implement. What is the advantage to write so many codes? Besides using parallelStream() as Code List 4.a may bring the advantage of parallel processing, this way does not bring much advantages. This reason is that the application (finding an object) is very simple, and using the Stream API is overkill.

The concept of Java Stream API is similar to the concept of Unix Pipeline or pipes and filters design pattern -- concatenating several simple operations to complete a meaning job. Since the operation is very simple, usually, using Lambda expression is concise and can improve the readability. As shown in Figure 1, Java Stream can concatenate serveral intermediate operations, and in the end, only one terminal operation as a pipeline. The intermediate operation is used to transforma the content of the stream, e.g., filtering (filter(Predicate)), mapping (map(Function), sorting (sorted(Comparator)), etc. And the terminal operation is used to produce the final result from or perform side effect on the content of the stream, e.g., collecting (collect(Collector)), applying something for each (forEach(Consumer)), or reducing (Reduce(BinaryOperator)), etc.

Figure 1 - Stream Pipeline

For example, a pipeline like Figure 2 can be used to summarize the assets of the rich persons who have assets of over 1 billion dollars. First, the filter(Predicate) filters out the persons who have assets of over 1 billion dollars. Then, the map(Function) extracts the value part of the assets. Finally, the reduce(BinaryOperation) aggregates values as the result. In fact, these similar operations are frequently used. Therefore, in Java Stream API, the Collectors class provides frequently-used terminal operations, e.g., summarizingDouble(ToDoubleFunction) combining a map(Function) intermediate operation and a terminal operation reduce(BinaryOperation) with the default implementation to simplify the composition of a pipeline.

Figure 2 - Stream Pipeline Example

The example is not concrete enough? One more concrete example. Assume that Exam represents a kind of examination, and a person can take an examination many times. Thus, in Person, a List is used to keep all examinations taken by the examinee. How to get the rank of the examinees whose score was more than 700 in any taken examination? To eliminate duplicated code, the getHighestScore() method like Code List 5 is added into Person to get the highest score in the taken examination (using the Stream API, too).

Then, a method showRank(List<Person>, double) can be written as Code List 6. The first parameter is the list of all examinees, and the second parameter is the score threshold required to show on the rank. The program first calls the stream() method to obtain the Stream object, and calls filter(Predicate) method of the Stream object to filter out the examinee whose score is under the threshold. Here, using the Lambda Expression to write the predicate function is intuitive and improves the readability. Call the sorted(Comparator) method to sort the examinees based on the score, and then the map(Function) method combining the examinee's fullname and score, e.g., "Spirit Tu: 840.0," as the result. Note that the element in the stream returned by the map(Function) method is not Person object anymore -- the element becomes a string object. Therefore, the Lambda Expression in forEach(Consumer), e represents a string and can be printed on the console directly. Finally, call showRank(persons, 700) to show the rank of examinees who ever got score more than 700 in one examination. The entire process of Code List 6 can illustrated as the pipeline in Figure 3.

Figure 3 - The pipeline of Code List 6

Honestly, I feel very kind of Java Stream API because I studied visual dataflow language many years in graduate school. In VisualTPL (my study), the concept of loop is implicit. What to do is more important that how to do. In the same way, Java Stream API internalizes the loop, the importance of an operation is to do what. Both improve the abstraction level and readability largely. However, the features provided by Java Stream API are more than that described in this article. The next article will describe other features.

almost 5 years ago

## Java 8 初探 - Stream

Java Stream API的概念類似Unix Pipelinepipes and filters design pattern，透過串接多個簡單的operation完成有意義的工作，由於operation通常都很簡單，所以使用Lambda expression多數時候可以帶來簡潔和提升可讀性的好處。如Figure 1所示，Java Stream能串多個intermediate operations，但最後只能串一個terminal operation來組成pipeline。用intermediate operation轉換stream內容，例如：過濾(filter(Predicate))、替換(map(Function)、排序(sorted(Comparator))等，然後用terminal operation對stream內的資料計算最終結果或產生side effect，例如：收集(collect(Collector))、逐一改變(forEach(Consumer))或歸納(Reduce(BinaryOperator))等。

Figure 1 - Stream Pipeline

Figure 2 - Stream Pipeline Example

Figure 3 - The pipeline of Code List 6

almost 5 years ago

## Quick Glance of Java 8 - Closure

In my impression of the offical bimonthly "Java Manazine," many articles discussed the Lambda expression coming with Java 8, but the closure was not been discussed much. With a rough search, I just found the closure mentioned in the article of discussing the Lambda expression in the 2013 July-August issue. I think the phenomenon comes from two reasons: (1) without Lambda, the anonymous class still can capture the (effectively final) variables; (2) the Lambda expression captured variables are still effectively final. Since the free variables don't perfect work, the closure is not an important point to be advertised with the Java Lambda expression. In the description of the Closure on the Wikipeida (not formal enough, but easy to acess than the books of programming languages), the closure can manipulate the captured free variables just like normal variables. However, the supports of the free variables are not the same in all languages.

Objective C, the language I use much in my work as an example. Through the concept of the shared storage, Apple offical document gives some examples to discuss the variables captured by the code blocks. Well, the concept of the memory storage is an unavoidable flaw for the language sensitive to memory address. However, in this article, I try to use a more abstract description to discuss the variables captured by code blocks. By default, the code block only captures the value of the variable, so any change to the variable after the block is not seen by the block. Thus, in Code List 1, the result of the NSLog is 42, not 84.

To capture the variable, not its value, in the code block, a modifier __block should be placed on the variable declaration like Code List 2. With the modifier, the result of NSLog becomes 84 because when the code block executed (line 6), the captured variable anInteger has been changed to 84 (line 5).

With the __block modifier, the code block captured the variable, not its value, the value of the variable can be modified inside the code block. Therefore, in Code List 3, the NSLog is called after the execution of callback(), the displayed result of anInteger is 100. I think the design has both advantages and disadvantages. For the language abstraction, the need of the __block modifier forces the programmers be aware of the existence of the memory address that lowers the language abstraction (non-intuitive). However, Objective C can use the modifier to optimize the compilation for performance. Objective C can provide both read-only and read-write captured variables, but also compile-time check, e.g., without __block modifier, any change to the captured variable is seen as compile error. Therefore, I think the __block modifier is not a bad design .

Well, it is time back to Java. As mentioned before, without Lambda, the anonymous class can capture the scope-visible vairable x like Code List 4, but the problem is that the captured variable x is effectively final. Therefore, to uncomment the line x = 48; will get a compile error.

Even with Lambda, Code List 5 as an example, the variable x in the Lambda is still a final variable, unable to modify the value. To uncomment the line x = 48; will still get a compile error

However, the final modifier in Java only limits the change to the variable. If the variable is an object reference, calling the methods of the object is allowed, even that the method will change the status of the object. Thus, Code List 4 can be modified to Code List 6. The same, trying to uncomment the line x = new AtomicInteger(48); will get a compile error, but use x.set(48); can change the value of x. (This also applies to Objective C)

Therefore, Code List 5 can be also modified to Code List 7. Unfortunately, the data types that support Autoboxing and Unboxing, e.g., Integer, are immutable data type. To use captured variables is not as convenient as JavaScript, or other languages that treat primitive types as objects. However, this way can provide some kinds of free variables similar.

Although Java 8 supports Lambda expression, I think the closure is still not the first-class citizen of Java. And as mentioned in the previous article, for testability, besides the one-line Lambda or the code too simple to break, I still like to write small and easy-to-test classes, not Lambda expression. To capture variables? Injecting variables through the constructor can be considered as a good way. As for when to write one-line Lambda or the code too simple to break, I think Stream, the new Collection API, opens the large possibility.

almost 5 years ago

## Java 8 初探 - Closure

__block修飾字讓code block捕捉變數本身，所以也可以更動變數的值，因此Code List 3中NSLog是在callback()執行後才顯示anInteger的值，結果是100。我個人覺得這樣的處理有好有壞，就抽象程度上，額外需要__block修飾字讓工程師還是意識到記憶體位置的存在，這是降低語言的抽象程度(不直覺)。Objective C可用這些修飾字針對效能提升最佳化編譯結果，不但能同時提供唯讀/讀寫的捕捉變數，另外也提供編譯期間的檢查，例如沒用__block但卻在code block中變更變數值會視為錯誤，某種程度上我覺得是還不錯的設計。

Java的final修飾字僅限制無法改變變數值，但若變數是個物件，呼叫物件method卻是允許的，即使該method會改變物件內的狀態都是允許的，所以Code List 4可以改寫成Code List 6。同樣，取消x = new AtomicInteger(48);的註解會得到編譯錯誤，但用x.set(48);可以實際改變x的值。(這一點Objective C也是一樣)

almost 5 years ago

## Quick Glance of Java 8 - Lambda

Finally, Java 8 released at March 18, 2014. However, after I developing iOS App in the company, I did not often use Java and had no time to use the beta version of Java 8. Recently, I'm interested in Java 8 and study its new features. One of the highlighted features of Java 8 is the Lambda expression. The Lambda expression can be seen in many languages. I wrote programs in the Lambda expression form most with JavaScript and Objective C (as known as code block). Honestly, I don't like the Lambda very much. I feel okay with the Lambda expression in JavaScript (by passing named function), but I feel the in-place code block of Objective C like spaghetti. In most cases, I use methods that return a code block to keep the program well-structured.

Well, Lambda becomes a part of Java 8, and what changes will make in writing Java programs? Let's see the program before Java 8, sorting a list as an example, to sort a List, the programmer needs to write a class that implements Comparator interface (the IntegerComparator in Code List 1), and then create an instance ascendingComparator as the parameter of List.sort() (see the sortWithComparator method in Code List 2). This is why many programmers say that Java is less productivity. However, I like to program in this way -- writing a lot of small classes because a small class is easy to write, easy to test with JUnit, and easy to reuse.

If the programmer do not want to write a class independently, he/she can write an anonymous class before Java 8, like the sortWithAnonymousClass method in Code List 2. I used this way to write programs at the beginning of learning Java with IDE like JBuilder -- drag-and-drop to design a UI, double-click on the control, and write some programs at the place that IDE auto-generated (yes, I learned the Visual Basic 6 in a similar way). After I learned the formal object-oriented analysis and design, I only use this way in the case that the anonymous class does not affect the overall design. However, many programmers still think the way is less productivity.

With the Lambda expression in Java 8, the sorting program can be written like the sortWithLambdaExpression method in Code List 2. The lines of code reduced and many programmers think that the readability also improved. The reason is that the sorting implementation is right there, you don't have to jump to another class (file) to read the implementation. I think that the readability improved only when the the logic wrapped by anonymous Lambda is very simple. If the logic is very complicated, put two different logics (logic outside the Lambda and the logic inside the Lambda) together only increase the length of code to read and reduce the readability. In that case, a better way is to extract the logic in the Lambda to a meaningful class. I'm used to the form: (arguments) -> {implementation} to write an anonymous Lambda -- I think that is because I had seen a lot of code blocks with Objective C.

Besides anonymous Lambda, Java 8 also support to pass the existing method as the Lambda, called method reference . For example, the sortWithMethodReference and sortWithStaticMethodReference methods in Code List 2. Comparing the sortWithLambdaExpression and sortWithMethodReference methods, which one is more readable? In my personal opinion, I think sortWithMethodReference is more readable than sortWithLambdaExpression because the method name can tell me that the numbers is sorted from small to large. I usually write Lambda with JavaScript in this way -- passing a meaningful named function.

Someone may say that what about closure? I want to discuss the closure in the next article. This article only discussed the productivity and the readability affected by Lambda. For the readability, as mentioned before, simple logic wrapped with Lambda indeed improves the reabability, but put complicated logic wrapped with Lambda and the other logic together will reduce the readability. For productivity, Lambda can write less codes (the basic requirement of a class declaration). Therefore, when the wrapped logic is simple, the productivity improvement is significant. For example, in Code List 1, the compare method only has one line, to declare a class for that line costs a lot. However, if the wrapped logic is complicated, the productivity improvement is not so important.

In the long term, writing less codes may not improve the productivity, but well-testable codes improve the productivity. This is also the reason I use methods to return code blocks when I developing iOS App. I believe that increasing the testability of a program can improve much more productivity than writing less codes because the time to maintain an existing program is longer than that to write new codes. As shown in Code List 3, doSomething may be an integration method and an anonymous Lambda wrapped logic inside the method. In order to test the wrapped logic, the only way is to test the entire doSomething method. If the logic is wrapped as independent class like IntegerComparator, it is easy to test the logic by test IntegerComparator directly without testing the integration method doSomething. For testability, using method reference is a better way than using anonymous Lambda.

Someone may think the sample code in Code List 3 is not a good example. There are other ways to improve the testability for Code List 3 indeed, but, I still think the anonymous Lambda does not provide a good testablity. Therefore, I prefer the method reference way to write Lambda with Java 8. Besides testability, I can follow the OO principles, e.g., single responsibility principle, to put the methods in the appropriate classes, and then use the method reference to use the methods as Lambda. As a result, the method reference approach increases readability (with meaningful method name), maintainability (good OO desing and easy to test) and also reusability (anonymous Lambda can not be reused, but method reference can reuse methods).

almost 5 years ago

## Java 8 初探 - Lambda

Java 8終於在2014的3月18日正式釋出了，不過自從用Objective C開發iOS App後，我已經有好一陣子沒碰Java，期間曾經有短暫寫一點點，但卻沒有時間去用beta版的Java 8，直到最近才又開始玩一下。Java 8最亮眼的特色之一應該就是所謂的Lambda表示法，Lambda表示法幾乎內建在很多語言中，而我用最多的應該在JavaScript和Objective C (code block)中了。但老實說，我對於Lambda其實不怎麼有愛，JavaScript版的寫法我覺得還好，但Objective C的in place code block我看了覺得好亂，後來大多數的情況下，我都用method回傳code block的方式在使用。

Lambda出現後，程式變成sortWithLambdaExpression函式所示那樣，確實行數減少不少，也有人認為可讀性提高不少，認為可讀性提高的原因是，程式碼就在那裡，不像過去還需要跳到另外一個class才能看到實作。關於可讀性這一點，我覺得只有在Lambda內的程式邏輯很簡單才成立，如果邏輯很複雜，把兩個不同邏輯的程式碼放在一起，不僅長度變長，可讀性反而降低，與其這樣還不如抽出來成為一個class並給予一個有意義的class名稱。至於語法用(arguments) -> {implementation}表示，可能是Objective C的code block看多了，有比剛開始第一次看到Java Lambda表示法稍微習慣多了。

Java 8除了這種匿名的Lambda表示法，其實也是支援將既有函式當成Lambda使用的用法，例如Code List 2中的sortWithMethodReferencesortWithStaticMethodReference。不知道大家覺得sortWithLambdaExpressionsortWithMethodReference兩相比較下，哪個可讀性較高呢？我個人是覺得sortWithMethodReference比較高，因為從函式的名稱就可以知道是以升冪的方式排序。我個人在寫JavaScript時也是比較喜歡這種寫法。