Java8容許開發者經過使用關鍵字default向接口中加入非抽象方法。這一特性被稱爲擴展方法。html
interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } //方法啓用 Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0
這裏要特別說明,並非全部的接口都會有效,java
當使用mybatis時,在dao層接口中寫默認實現是沒法調用的,依然會報錯,org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): io.creams.building.dao.RoomLogDao.add,依然會從mapper.xml文件中尋找對應的語句,不會執行默認實現方法。apache
當使用JPA(hibernate不肯定,沒測試),是可使用的,會執行默認方法。可是沒法在接口中注入EntityManager。能夠經過上下文得到,能夠完成自定義Criteria查詢c#
1:功能簡介api
例如:一般的排序功能數組
List<String> names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator<String>() { @Override public int compare(String a, String b) { return b.compareTo(a); } });
採用java8能夠簡化這個--lambda表達式安全
Collections.sort(names, (String a, String b) -> b.compareTo(a)); Collections.sort(names, (a, b) -> b.compareTo(a));
2:lambda表達式的方法與構造方法mybatis
(1):功能性接口多線程
lambda表達式是如何和java系統的類型進行對應的?每一個lambda表達式都對應一個指定的類型,這個指定的類型是由接口肯定的。該接口被稱之爲功能性接口,它必須且剛好只包含一個抽象方法聲明。被指定接口類型所對應的lambda表達式恰好和這個接口的抽象方法想匹配。由於默認方法不是抽象的,所以你能夠在你的功能性接口中添加多個默認方法。併發
咱們能夠將任意的接口用做lambda表示式,只要該接口僅僅包含一個抽象方法。爲了確保你定義的接口達到要求,你能夠在接口上添加 @FunctionInterface
註解。編譯器能夠檢測到該註解並判斷你的接口是否知足條件,若是 你定義的接口包含多個抽象方法時,編譯器便會報錯。
示例:
@FunctionalInterface interface Converter<F, T> { T convert(F from); }
Converter<String, Integer> converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123
(2):方法與構造方法
前部分的示例在使用靜態方法引用的狀況下能夠被進一步的簡化:
Converter<String, Integer> converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123
java8可讓你經過關鍵字::來傳遞方法和構造函數的引用。上面的示例展現瞭如何引用一個靜態方法。咱們一樣也能夠引用對象方法。
class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); } }
Something something = new Something(); Converter<String, String> converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J
接下來咱們定義一個用來建立類person的工廠接口:
interface PersonFactory<P extends Person> { P create(String firstName, String lastName); }
不使用一般的手動實現工廠類,咱們經過使用構造函數將全部的工做聯合在一塊兒:
PersonFactory<Person> personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");
咱們經過Person::new建立一個指向Person構造函數的引用。java編譯器自動的選擇正確的構造函數來匹配PersonFactory.create的函數簽名。
在lambda表達式裏訪問外部變量和匿名類的方式是十分類似的。你能夠在lambda中訪問外部的final變量,訪問實例字段和靜態變量的方法也是如此。
咱們能夠訪問在lambda表示式以外的本地final變量:
final int num = 1; Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3
可是和匿名變量不一樣的是變量num沒必要強制的被聲明爲final。下面的代碼依然是合法的:
int num = 1; Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3
可是實際上,變量num在編譯期是被隱式的轉換爲fianl類型的。下面的代碼是不能被成功的編譯的:
int num = 1; Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num); //這裏的num會編譯報錯 num = 3;
在lambda表達式內部向變量num寫入值一樣是不容許的。
和訪問本地變量相反,咱們在lambda表達式裏便可以讀取也能夠寫入對象字段和靜態變量。這一準則一樣適用於匿名類。
class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter<Integer, String> stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter<Integer, String> stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
還記得第一部分的formula示例麼?接口formula定義了一個默認方法sqrt,這個方法能夠被formula的實例和匿名實例所訪問。 可是這個方法不能被lambda表達式所訪問。
默認方法不能被lambda表示式內部的代碼訪問。下面的代碼不能經過編譯。
Formula formula = (a) -> sqrt( a * 100);
JDK1.8包括了許多功能性接口。它們中的一些是老版本中被熟知的接口,例如Comparator和Runnable。這些已存在的接口已經經過@FunctionalInterface註解擴展爲支持Lambda表達式。
同時Java8的API也包含了不少新的功能性接口簡化你的開發。一些新的接口是來自很是出名的Google Guava庫。即便你已經對這庫十分熟悉了,你也應當留意下這些接口是如何被擴展的。
Predicates是隻擁有一個參數的Boolean型功能的接口。這個接口擁有多個默認方法用於構成predicates複雜的邏輯術語。
Predicate<String> predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate<Boolean> nonNull = Objects::nonNull; Predicate<Boolean> isNull = Objects::isNull; Predicate<String> isEmpty = String::isEmpty; Predicate<String> isNotEmpty = isEmpty.negate();
Functions接受一個參數併產生一個結果。默認方法可以用於將多個函數連接在一塊兒。
Function<String, Integer> toInteger = Integer::valueOf; Function<String, String> backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
Suppliers對於給定的泛型類型產生一個實例。不一樣於Functions,Suppliers不須要任何參數。
Supplier<Person> personSupplier = Person::new; personSupplier.get(); // new Person
Consumers表明在只有一個輸入參數時操做被如何執行。
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
Comparators在老版本中就已經被熟知。Java8向該接口中添加了多種默認方法。
Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
Optionals並非功能性接口,反而它是一種特殊的工具用來阻止NullPointerException。咱們首先快速的瀏覽Optionals是如何工做的,由於它在下一章節是十分重要的概念。
Optional是一種能夠包含null和non-null值的簡單容器。考慮到方法能夠返回non-null結果,偶爾也可能任何都不返回。在Java8中,你能夠返回Optional而不是返回null。
Optional<String> optional = Optional.of("bam"); optional.isPresent(); // true optional.get(); // "bam" optional.orElse("fallback"); // "bam" optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"
java.util.Stream表明着一串你能夠在其上進行多種操做的元素。流操做既能夠是連續的也能夠是中斷的。中斷操做返回操做結果。而連續操做返回流自己,這樣你就能夠在該行上繼續操做。流是建立在數據源上的,例如:java.util.Collection、list集合和set集合。流操做既能夠順序執行也能夠並行執行。 咱們首先了解下順序的流是如何工做的。咱們首先建立一個字符串鏈表。
List<String> stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
Java8的Collections類已經被擴展了,你能夠簡單的調用Collection.stream()或者Collection.parallelSteam()來建立流。下面部分將介紹大部分流操做。
Filter接受一個predicate來過濾流中的全部元素。這個操做是連續的,它可讓咱們在結果上繼續調用另一個流操做forEach。ForEach接受一個consumer,它被用來對過濾流中的每一個元素執行操做。ForEach是一箇中斷操做,所以咱們不能在ForEach後調用其餘流操做。
stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
Sorted是一個連續操做,它返回流的已排序版本。若是你沒有顯示的指定Comparator,那麼流中元素的排序規則爲默認的。
stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2"
須要注意的是sorted只建立了流的排序結果,它並無改變集合中元素的排序位置。stringCollection中元素排序是沒有改變的。
System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
連續性操做map經過指定的Function將流中的每一個元素轉變爲另外的對象。下面的示例將每一個字符串轉換爲大寫的字符串。此外,你也可使用map將每一個元素的類型改變爲其它類型。轉換後流的泛型類型依賴於你傳入的Function的泛型類型。
stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
各類匹配操做能夠用來檢測是否某種predicate和流中元素相匹配。全部的這些操做是中斷的並返回一個boolean結果。
boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
Count是中斷型操做,它返回流中的元素數量。
long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
這個中斷性操做使用指定的function對流中元素實施消減策略。此操做的返回值是一個包括全部被消減元素的Optional。
Optional<String> reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
在前面部分咱們提到流能夠是順序的也能夠是並行的。順序流的操做是在單線程上執行的,而並行流的操做是在多線程上併發執行的。
隨後的例子咱們展現了並行流能夠多麼容易的提升性能。
首先,咱們建立一個包含惟一元素的大容器:
int max = 1000000; List<String> values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
如今咱們開始測試排序這些元素須要多長時間。
Sequential Sort
long t0 = System.nanoTime(); long count = values.stream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); // sequential sort took: 899 ms
Parallel Sort
long t0 = System.nanoTime(); long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms
你會觀察到這兩種模式的代碼基本上是一致的,可是並行排序所花費的時間大約是順序排序的一半。
咱們已經提到maps不支持流。然而如今maps包括了許多新的很是有用的方法用於執行通用任務。
Map<Integer, String> map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val));
上述的代碼應該很清晰了:putIfAbsent使得咱們不用寫是否爲null值的檢測語句;forEach使用consumer來對map中的每一個元素進行操做。下面的例子向咱們展現使用功能性函數在map裏執行代碼:
map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33
接下來,咱們將學習如何刪除給定鍵所對應的元素。刪除操做還須要知足給定的值須要和map中的值想等:
map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null
其餘一些幫助性方法:
map.getOrDefault(42, "not found"); // not found
合併map中的實體是十分容易的:
map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
若是map不存在指定的鍵,那麼它將把該鍵值對key/value加入map中。反而,若是存在,它將調用function來進行合併操做。
Java8在包java.time下面包括了一款新的date和time的API。新的Date API和Joda-Time庫是相兼容的,可是它們不是同樣的。下面的示例覆蓋了新API中的重要部分。
Clock提供了訪問當前日期和時間的方法。Clock是時區敏感的而且它能夠被用來替代System.currentTimeMillis進行獲取當前毫秒數。同時,時間軸上的時間點是能夠用類Instant來表示的。Instants能夠被用來建立遺留的java.util.Date對象。
Clock clock = Clock.systemDefaultZone(); long millis = clock.millis(); Instant instant = clock.instant(); Date legacyDate = Date.from(instant); // legacy java.util.Date
TimeZones被用來表示ZoneId。它們能夠經過靜態工廠方法訪問。TImeZones定義了時差,它在instants和本地日期時間轉換上十分重要。
System.out.println(ZoneId.getAvailableZoneIds()); // prints all available timezone ids ZoneId zone1 = ZoneId.of("Europe/Berlin"); ZoneId zone2 = ZoneId.of("Brazil/East"); System.out.println(zone1.getRules()); System.out.println(zone2.getRules()); // ZoneRules[currentStandardOffset=+01:00] // ZoneRules[currentStandardOffset=-03:00]
本地時間表明瞭一個和時區無關的時間,e.g. 10pm or 17:30:15. 下面的示例建立了前部分展現的兩個時區的本地時間。而後,咱們將比較這兩個時間並計算出這兩個時間在小時和分鐘數上的差別。
LocalTime now1 = LocalTime.now(zone1); LocalTime now2 = LocalTime.now(zone2); System.out.println(now1.isBefore(now2)); // false long hoursBetween = ChronoUnit.HOURS.between(now1, now2); long minutesBetween = ChronoUnit.MINUTES.between(now1, now2); System.out.println(hoursBetween); // -3 System.out.println(minutesBetween); // -239
LocalTime包含了多個工廠方法用來簡化建立過程,其中也包括經過字符串來建立時間:
LocalTime late = LocalTime.of(23, 59, 59); System.out.println(late); // 23:59:59 DateTimeFormatter germanFormatter = DateTimeFormatter .ofLocalizedTime(FormatStyle.SHORT) .withLocale(Locale.GERMAN); LocalTime leetTime = LocalTime.parse("13:37", germanFormatter); System.out.println(leetTime); // 13:37
LocalDate表明了一個可區分日期,e.g. 2014-03-11。 它是不變的同時工做原理相似於LocalTime。下面的例子描繪了經過加減年,月,日來計算出一個新的日期。須要注意的是這每一個操做都返回一個新的實例。
LocalDate today = LocalDate.now(); LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS); LocalDate yesterday = tomorrow.minusDays(2); LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4); DayOfWeek dayOfWeek = independenceDay.getDayOfWeek(); System.out.println(dayOfWeek); // FRIDAY
從字符串解析出LocalDate和解析LocalTime同樣簡單:
DateTimeFormatter germanFormatter = DateTimeFormatter .ofLocalizedDate(FormatStyle.MEDIUM) .withLocale(Locale.GERMAN); LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter); System.out.println(xmas); // 2014-12-24
LocalDateTime表明日期和時間。它將咱們前部分看到的時間和日期組合進一個實例。LocalDateTime是不可變的而且它的工做原理和LocalTime和LocalDate十分類似。 咱們能夠從date-time中獲取某些字段值:
LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59); DayOfWeek dayOfWeek = sylvester.getDayOfWeek(); System.out.println(dayOfWeek); // WEDNESDAY Month month = sylvester.getMonth(); System.out.println(month); // DECEMBER long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY); System.out.println(minuteOfDay); // 1439
在一些額外的時區信息幫助下,它能夠被轉換爲instant。Instants能夠被容易的轉換爲遺留的java.util.Date類型。
Instant instant = sylvester .atZone(ZoneId.systemDefault()) .toInstant(); Date legacyDate = Date.from(instant); System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014
格式date-time的過程和格式date和time基本上是同樣的。在使用系統自帶的定義格式時,咱們也能夠定義咱們本身的格式:
DateTimeFormatter formatter = DateTimeFormatter .ofPattern("MMM dd, yyyy - HH:mm"); LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter); String string = formatter.format(parsed); System.out.println(string); // Nov 03, 2014 - 07:13
和java.text.NumberFormat不同的是DateTimeFormatter是不可變的而且是類型安全的。
若是想了解詳細的格式語法,能夠閱讀這裏。
Java8中的Annotations是可重複。如今咱們深刻的學習一個例子來理解它。
首先,咱們定義一個包裝註解,它包含了一個實際註解的數組。
@interface Hints { Hint[] value(); } @Repeatable(Hints.class) @interface Hint { String value(); }
Java8可使同一個註解類型同時使用屢次,只要咱們在註解聲明時使用@Repeatable。
情景1:使用容器註解
@Hints({@Hint("hint1"), @Hint("hint2")}) class Person {}
情景2:使用可重複註解
@Hint("hint1") @Hint("hint2") class Person {}
在第二種情景下,java編譯器隱式的在該註解使用中加入@Hints。這種後期處理在經過反射獲取註解是十分重要的。
Hint hint = Person.class.getAnnotation(Hint.class); System.out.println(hint); // null Hints hints1 = Person.class.getAnnotation(Hints.class); System.out.println(hints1.value().length); // 2 Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class); System.out.println(hints2.length); // 2
雖然咱們歷來沒有在類Person上聲明@Hints註解,但該信息仍是能夠經過getAnnotation(Hint.class)得到。 此外,getAnnotationsByType是一種更加便利的方法,它能夠保證咱們訪問全部使用的@Hint註解。
此外,Java8中註解的使用範圍擴展到兩種新的類型:
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @interface MyAnnotation {}
總結 個人Java8語言特性教程到此就結束了。此外還有不少新的內容須要闡述。去不去了解JDK8中的這些很是棒的特性取決於你,這些特性包括有Arrays.parallelSort,StampedLock,CompletableFuture ---即便列舉名字也有不少了。我已經在網站上把這些特性都列舉出來了,你能夠去哪裏看看。