资讯专栏INFORMATION COLUMN

Java实战之Java8指南

nemo / 1326人阅读

摘要:首先我们定义一个有两个不同控制器的然后,我们创建一个特定的工厂接口来创建新的对象不需要手动的去继承实现该工厂接口,我们只需要将控制器的引用传递给该接口对象就好了的控制器会自动选择合适的构造器方法。这种指向时间轴的对象即是类。

</>复制代码

  1. 本文为翻译文章,原文地址 这里

欢迎来到本人对于Java 8的系列介绍教程,本教程会引导你一步步领略最新的语法特性。通过一些简单的代码示例你即可以学到默认的接口方法、Lambda表达式、方法引用以及重复注解等等。本文的最后还提供了譬如Stream API之类的详细的介绍。

Default Methods for Interfaces

Java 8 允许我们利用default关键字来向接口中添加非抽象的方法作为默认方法。下面是一个小例子:

</>复制代码

  1. interface Formula {
  2. double calculate(int a);
  3. default double sqrt(int a) {
  4. return Math.sqrt(a);
  5. }
  6. }

在接口Formula中,除了抽象方法calculate之外,还定义了一个默认的方法sqrt。实现类只需要实现抽象方法calculate,而sqrt方法可以跟普通方法一样调用。

</>复制代码

  1. Formula formula = new Formula() {
  2. @Override
  3. public double calculate(int a) {
  4. return sqrt(a * 100);
  5. }
  6. };
  7. formula.calculate(100); // 100.0
  8. formula.sqrt(16); // 4.0
Lambda表达式

首先以简单的字符串排序为例来展示:

</>复制代码

  1. List names = Arrays.asList("peter", "anna", "mike", "xenia");
  2. Collections.sort(names, new Comparator() {
  3. @Override
  4. public int compare(String a, String b) {
  5. return b.compareTo(a);
  6. }
  7. });

静态的工具类方法Collections.sort接受一个列表参数和一个比较器对象来对于指定列表中的元素进行排序。我们常常需要创建匿名比较器并且将他们传递给排序方法。而Java 8中提供的一种更短的Lambda表达式的方法来完成该工作:

</>复制代码

  1. Collections.sort(names, (String a, String b) -> {
  2. return b.compareTo(a);
  3. });

可以发现用如上的方法写会更短并且更加的可读,并且可以更短:

</>复制代码

  1. Collections.sort(names, (String a, String b) -> b.compareTo(a));

这种写法就完全不需要{}以及return关键字,再进一步简化的话,就变成了:

</>复制代码

  1. names.sort((a, b) -> b.compareTo(a));
Functional Interfaces

Lambda表达式是如何适配进Java现存的类型系统的呢?每个Lambda表达式都会关联到一个由接口确定的给定的类型。这种所谓的函数式接口必须只能包含一个抽象方法,而每个该类型的Lambda表达式都会关联到这个抽象方法。不过由于默认方法不是抽象的,因此可以随便添加几个默认的方法到函数式接口中。我们可以将任意的接口作为Lambda表达式使用,只要该接口仅含有一个抽象方法即可。为了保证你的接口满足这个需求,应该添加@FunctionalInterface这个注解。编译器会在你打算向某个函数式接口中添加第二个抽象方法时候报错。

</>复制代码

  1. //声明
  2. @FunctionalInterface
  3. interface Converter {
  4. T convert(F from);
  5. }
  6. //使用
  7. Converter converter = (from) -> Integer.valueOf(from);
  8. Integer converted = converter.convert("123");
  9. System.out.println(converted); // 123
Method and Constructor References

上述的代码可以使用静态方法引用而更加的简化:

</>复制代码

  1. Converter converter = Integer::valueOf;
  2. Integer converted = converter.convert("123");
  3. System.out.println(converted); // 123

Java 8允许将方法或则构造器的引用通过::关键字进行传递,上述的例子是演示了如何关联一个静态方法,不过我们也可以关联一个对象方法:

</>复制代码

  1. class Something {
  2. String startsWith(String s) {
  3. return String.valueOf(s.charAt(0));
  4. }
  5. }

</>复制代码

  1. Something something = new Something();
  2. Converter converter = something::startsWith;
  3. String converted = converter.convert("Java");
  4. System.out.println(converted); // "J"

接下来看 :: 关键字怎么在构造器中起作用。首先我们定义一个有两个不同控制器的Java Bean:

</>复制代码

  1. class Person {
  2. String firstName;
  3. String lastName;
  4. Person() {}
  5. Person(String firstName, String lastName) {
  6. this.firstName = firstName;
  7. this.lastName = lastName;
  8. }
  9. }

然后,我们创建一个特定的Person工厂接口来创建新的Person对象:

</>复制代码

  1. interface PersonFactory

    {

  2. P create(String firstName, String lastName);
  3. }

不需要手动的去继承实现该工厂接口,我们只需要将控制器的引用传递给该接口对象就好了:

</>复制代码

  1. PersonFactory personFactory = Person::new;
  2. Person person = personFactory.create("Peter", "Parker");

Java的控制器会自动选择合适的构造器方法。

Lambda Scopes

从Lambda表达式中访问外部作用域中变量非常类似于匿名对象,可以访问本地的final变量、实例域以及静态变量。

Accessing local variables

在匿名对象中,我们可以从Lambda表达式的域中访问外部的final变量。

</>复制代码

  1. final int num = 1;
  2. Converter stringConverter =
  3. (from) -> String.valueOf(from + num);
  4. stringConverter.convert(2); // 3

但是不同于匿名对象只能访问final变量,Lambda表达式中可以访问final变量:

</>复制代码

  1. int num = 1;
  2. Converter stringConverter =
  3. (from) -> String.valueOf(from + num);
  4. stringConverter.convert(2); // 3

不过需要注意的是,尽管变量不需要声明为final,但是也是隐式的不可变:

</>复制代码

  1. int num = 1;
  2. Converter stringConverter =
  3. (from) -> String.valueOf(from + num);
  4. num = 3;

譬如如上的写法就会被报错。

Accessing fields and static variables

不同于本地变量,我们可以在Lambda表达式中任意的读写:

</>复制代码

  1. class Lambda4 {
  2. static int outerStaticNum;
  3. int outerNum;
  4. void testScopes() {
  5. Converter stringConverter1 = (from) -> {
  6. outerNum = 23;
  7. return String.valueOf(from);
  8. };
  9. Converter stringConverter2 = (from) -> {
  10. outerStaticNum = 72;
  11. return String.valueOf(from);
  12. };
  13. }
  14. }
Accessing Default Interface Methods

注意,Lambda表达式中是不可以访问默认方法的:

</>复制代码

  1. Formula formula = (a) -> sqrt( a * 100);

上述代码是编译通不过的。

Built-in Functional Interfaces

JDK 1.8 的API中包含了许多的内建的函数式接口,其中部分的譬如Comparator、Runnable被改写成了可以由Lambda表达式支持的方式。除此之外,Java 8还添加了许多来自于Guava中的依赖库,并将其改造为了Lambda接口。

Predicates

Predicates是包含一个参数的返回为布尔值的接口,接口包含了许多的默认方法来进行不同的复杂的逻辑组合:

</>复制代码

  1. Predicate predicate = (s) -> s.length() > 0;
  2. predicate.test("foo"); // true
  3. predicate.negate().test("foo"); // false
  4. Predicate nonNull = Objects::nonNull;
  5. Predicate isNull = Objects::isNull;
  6. Predicate isEmpty = String::isEmpty;
  7. Predicate isNotEmpty = isEmpty.negate();
Functions

Functions接口接受一个参数并且产生一个结果,同样提供了部分默认的方法来链式组合不同的函数(compose,andThen)。

</>复制代码

  1. Function toInteger = Integer::valueOf;
  2. Function backToString = toInteger.andThen(String::valueOf);
  3. backToString.apply("123"); // "123"
Suppliers

</>复制代码

  1. Supplier personSupplier = Person::new;
  2. personSupplier.get(); // new Person
Consumers

</>复制代码

  1. Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName);
  2. greeter.accept(new Person("Luke", "Skywalker"));
Comparators

Comparators类似于旧版本中的用法,Java 8是添加了一些默认的用法。

</>复制代码

  1. Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);
  2. Person p1 = new Person("John", "Doe");
  3. Person p2 = new Person("Alice", "Wonderland");
  4. comparator.compare(p1, p2); // > 0
  5. comparator.reversed().compare(p1, p2); // < 0
Optionals

Optionals并不是一个函数式接口,但是非常有用的工具类来防止NullPointerException。Optional是一个简单的容器用于存放那些可能为空的值。对于那种可能返回为null的方法可以考虑返回Optional而不是null:

</>复制代码

  1. Optional optional = Optional.of("bam");
  2. optional.isPresent(); // true
  3. optional.get(); // "bam"
  4. optional.orElse("fallback"); // "bam"
  5. optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"
Streams

一个Java的Stream对象代表了一系列可以被附加多个操作的元素的序列集合。常用的Stream API分为媒介操作与终止操作。其中终止操作会返回某个特定类型的值,而媒介操作则会返回流本身以方便下一步的链式操作。Streams可以从java.util.Collection的数据类型譬如lists或者sets(不支持maps)中创建。而Streams的操作可以顺序执行也可以并发地执行。

</>复制代码

  1. Stream.js是一个利用JavaScript实现的Java 8的流接口。

首先我们创建待处理的数据:

</>复制代码

  1. List stringCollection = new ArrayList<>();
  2. stringCollection.add("ffffd2");
  3. stringCollection.add("aaa2");
  4. stringCollection.add("bbb1");
  5. stringCollection.add("aaa1");
  6. stringCollection.add("bbb3");
  7. stringCollection.add("ccc");
  8. stringCollection.add("bbb2");
  9. stringCollection.add("ffffd1");

接下来可以利用Collection.stream() or Collection.parallelStream()来转化为一个流对象。

Filter

Filter会接受一个Predicate对象来过滤流中的元素,这个操作属于媒介操作,譬如可以在该操作之后调用另一个流操作(forEach)。ForEach操作属于终止操作,接受一个Consumer对象来对于过滤之后的流中的每一个元素进行操作。

</>复制代码

  1. stringCollection
  2. .stream()
  3. .filter((s) -> s.startsWith("a"))
  4. .forEach(System.out::println);
  5. // "aaa2", "aaa1"
Sorted

Sorted操作属于一个媒介操作,会将流对象作为返回值返回。元素会默认按照自然的顺序返回,除非你传入了一个自定义的Comparator对象。

</>复制代码

  1. stringCollection
  2. .stream()
  3. .sorted()
  4. .filter((s) -> s.startsWith("a"))
  5. .forEach(System.out::println);
  6. // "aaa1", "aaa2"

需要记住的是,Sorted操作并不会改变流中的元素的顺序,只会创建一个经过排序的视图,譬如:

</>复制代码

  1. System.out.println(stringCollection);
  2. // ffffd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ffffd1
Map

map操作也是媒介操作的一种,可以通过给定的函数将每个元素映射到其他对象。下面的代码示例就是将所有的字符串转化为大写字符串。不过map操作是可以将任意对象转化为任意类型,流返回的泛型类型取决于传递给map的函数的返回值。

</>复制代码

  1. stringCollection
  2. .stream()
  3. .map(String::toUpperCase)
  4. .sorted((a, b) -> b.compareTo(a))
  5. .forEach(System.out::println);
  6. // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Match

Java 8提供了一些列的匹配的终止操作符来帮助开发者判断流当中的元素是否符合某些判断规则。所有的匹配类型的操作都会返回布尔类型。

</>复制代码

  1. boolean anyStartsWithA =
  2. stringCollection
  3. .stream()
  4. .anyMatch((s) -> s.startsWith("a"));
  5. System.out.println(anyStartsWithA); // true
  6. boolean allStartsWithA =
  7. stringCollection
  8. .stream()
  9. .allMatch((s) -> s.startsWith("a"));
  10. System.out.println(allStartsWithA); // false
  11. boolean noneStartsWithZ =
  12. stringCollection
  13. .stream()
  14. .noneMatch((s) -> s.startsWith("z"));
  15. System.out.println(noneStartsWithZ); // true
Count

Count 也是终止操作的一种,它会返回流中的元素的数目。

</>复制代码

  1. long startsWithB =
  2. stringCollection
  3. .stream()
  4. .filter((s) -> s.startsWith("b"))
  5. .count();
  6. System.out.println(startsWithB); // 3
Reduce

该操作根据指定的方程对于流中的元素进行了指定的减少的操作。结果是Optional类型。

</>复制代码

  1. Optional reduced =
  2. stringCollection
  3. .stream()
  4. .sorted()
  5. .reduce((s1, s2) -> s1 + "#" + s2);
  6. reduced.ifPresent(System.out::println);
  7. // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ffffd1#ffffd2"
Parallel Streams

正如上文中提及的,流可以是顺序的也可以是并行的。所有在顺序流上执行的流操作都是在单线程中运行的,而在并行流中进行的操作都是在多线程中运行的。如下的代码演示了如何利用并行流来提供性能,首先我们创建一个待比较的序列:

</>复制代码

  1. int max = 1000000;
  2. List values = new ArrayList<>(max);
  3. for (int i = 0; i < max; i++) {
  4. UUID uuid = UUID.randomUUID();
  5. values.add(uuid.toString());
  6. }
Sequential Sort

</>复制代码

  1. long t0 = System.nanoTime();
  2. long count = values.stream().sorted().count();
  3. System.out.println(count);
  4. long t1 = System.nanoTime();
  5. long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
  6. System.out.println(String.format("sequential sort took: %d ms", millis));
  7. // sequential sort took: 899 ms
Parallel Sort

</>复制代码

  1. long t0 = System.nanoTime();
  2. long count = values.parallelStream().sorted().count();
  3. System.out.println(count);
  4. long t1 = System.nanoTime();
  5. long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
  6. System.out.println(String.format("parallel sort took: %d ms", millis));
  7. // parallel sort took: 472 ms
Maps

正如上文所说,Map并不支持流操作,但是也提供了很多有用的方法来进行通用的操作。

</>复制代码

  1. Map map = new HashMap<>();
  2. for (int i = 0; i < 10; i++) {
  3. map.putIfAbsent(i, "val" + i);
  4. }
  5. map.forEach((id, val) -> System.out.println(val));

上述的代码中,putIfAbsent避免了写太多额外的空检查。forEach会接受一个Consumer参数来遍历Map中的元素。下面的代码演示如何进行计算:

</>复制代码

  1. map.computeIfPresent(3, (num, val) -> val + num);
  2. map.get(3); // val33
  3. map.computeIfPresent(9, (num, val) -> null);
  4. map.containsKey(9); // false
  5. map.computeIfAbsent(23, num -> "val" + num);
  6. map.containsKey(23); // true
  7. map.computeIfAbsent(3, num -> "bam");
  8. map.get(3); // val33

还有,Map提供了如何根据给定的key,vaue来删除Map中给定的元素:

</>复制代码

  1. map.remove(3, "val3");
  2. map.get(3); // val33
  3. map.remove(3, "val33");
  4. map.get(3); // null

还有一个比较有用的方法:

</>复制代码

  1. map.getOrDefault(42, "not found"); // not found

同时,Map还提供了merge方法来帮助有效地对于值进行修正:

</>复制代码

  1. map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
  2. map.get(9); // val9
  3. map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
  4. map.get(9); // val9concat
Date API

Java 8包含了一个全新的日期与时间的API,在java.time包下,新的时间API集成了Joda-Time的库。

Clock

Clock方便我们去读取当前的日期与时间。Clocks可以根据不同的时区来进行创建,并且可以作为System.currentTimeMillis()的替代。这种指向时间轴的对象即是Instant类。Instants可以被用于创建java.util.Date对象。

</>复制代码

  1. Clock clock = Clock.systemDefaultZone();
  2. long millis = clock.millis();
  3. Instant instant = clock.instant();
  4. Date legacyDate = Date.from(instant); // legacy java.util.Date
Timezones

Timezones以ZoneId来区分。可以通过静态构造方法很容易的创建,Timezones定义了Instants与Local Dates之间的转化关系:

</>复制代码

  1. System.out.println(ZoneId.getAvailableZoneIds());
  2. // prints all available timezone ids
  3. ZoneId zone1 = ZoneId.of("Europe/Berlin");
  4. ZoneId zone2 = ZoneId.of("Brazil/East");
  5. System.out.println(zone1.getRules());
  6. System.out.println(zone2.getRules());
  7. // ZoneRules[currentStandardOffset=+01:00]
  8. // ZoneRules[currentStandardOffset=-03:00]
LocalTime

LocalTime代表了一个与时间无关的本地时间,譬如 10pm 或者 17:30:15。下述的代码展示了根据不同的时间轴创建的不同的本地时间:

</>复制代码

  1. LocalTime now1 = LocalTime.now(zone1);
  2. LocalTime now2 = LocalTime.now(zone2);
  3. System.out.println(now1.isBefore(now2)); // false
  4. long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
  5. long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
  6. System.out.println(hoursBetween); // -3
  7. System.out.println(minutesBetween); // -239

LocalTime提供了很多的工厂方法来简化创建实例的步骤,以及对于时间字符串的解析:

</>复制代码

  1. LocalTime late = LocalTime.of(23, 59, 59);
  2. System.out.println(late); // 23:59:59
  3. DateTimeFormatter germanFormatter =
  4. DateTimeFormatter
  5. .ofLocalizedTime(FormatStyle.SHORT)
  6. .withLocale(Locale.GERMAN);
  7. LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
  8. System.out.println(leetTime); // 13:37
LocalDate

LocalDate代表了一个独立的时间类型,譬如2014-03-11。它是一个不可变的对象并且很类似于LocalTime。下列代码展示了如何通过增减时间年月来计算日期:

</>复制代码

  1. LocalDate today = LocalDate.now();
  2. LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
  3. LocalDate yesterday = tomorrow.minusDays(2);
  4. LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
  5. DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
  6. System.out.println(dayOfWeek); // FRIDAY

从字符串解析得到LocalDate对象也像LocalTime一样简单:

</>复制代码

  1. DateTimeFormatter germanFormatter =
  2. DateTimeFormatter
  3. .ofLocalizedDate(FormatStyle.MEDIUM)
  4. .withLocale(Locale.GERMAN);
  5. LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
  6. System.out.println(xmas); // 2014-12-24
LocalDateTime

LocalDateTime代表了时间日期类型,它组合了上文提到的Date类型以及Time类型。LocalDateTime同样也是一种不可变类型,很类似于LocalTime以及LocalDate。

</>复制代码

  1. LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
  2. DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
  3. System.out.println(dayOfWeek); // WEDNESDAY
  4. Month month = sylvester.getMonth();
  5. System.out.println(month); // DECEMBER
  6. long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
  7. System.out.println(minuteOfDay); // 1439

上文中提及的Instant也可以用来将时间根据时区转化:

</>复制代码

  1. Instant instant = sylvester
  2. .atZone(ZoneId.systemDefault())
  3. .toInstant();
  4. Date legacyDate = Date.from(instant);
  5. System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014

从格式化字符串中解析获取到数据对象,也是非常简单:

</>复制代码

  1. DateTimeFormatter formatter =
  2. DateTimeFormatter
  3. .ofPattern("MMM dd, yyyy - HH:mm");
  4. LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
  5. String string = formatter.format(parsed);
  6. System.out.println(string); // Nov 03, 2014 - 07:13
Annotations

Java 8中的注解现在是可以重复的,下面我们用例子直接说明。首先,创建一个容器注解可以用来存储一系列真实的注解:

</>复制代码

  1. @interface Hints {
  2. Hint[] value();
  3. }
  4. @Repeatable(Hints.class)
  5. @interface Hint {
  6. String value();
  7. }

通过添加 @Repeatable注解,就可以在同一个类型中使用多个注解。

Variant 1: Using the container annotation (old school)

</>复制代码

  1. @Hints({@Hint("hint1"), @Hint("hint2")})
  2. class Person {}
Variant 2: Using repeatable annotations (new school)

</>复制代码

  1. @Hint("hint1")
  2. @Hint("hint2")
  3. class Person {}

</>复制代码

  1. Hint hint = Person.class.getAnnotation(Hint.class);
  2. System.out.println(hint); // null
  3. Hints hints1 = Person.class.getAnnotation(Hints.class);
  4. System.out.println(hints1.value().length); // 2
  5. Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);
  6. System.out.println(hints2.length); // 2
Further Reading

Java 8 Stream Tutorial

Java 8 Nashorn Tutorial

Java 8 Concurrency Tutorial: Threads and Executors

Java 8 Concurrency Tutorial: Synchronization and Locks

Java 8 Concurrency Tutorial: Atomic Variables and ConcurrentMap

Java 8 API by Example: Strings, Numbers, Math and Files

Avoid Null Checks in Java 8

Fixing Java 8 Stream Gotchas with IntelliJ IDEA

Using Backbone.js with Java 8 Nashorn

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/64730.html

相关文章

  • 从小白程序员一路晋升为大厂高级技术专家我看过哪些书籍?(建议收藏)

    摘要:大家好,我是冰河有句话叫做投资啥都不如投资自己的回报率高。马上就十一国庆假期了,给小伙伴们分享下,从小白程序员到大厂高级技术专家我看过哪些技术类书籍。 大家好,我是...

    sf_wangchong 评论0 收藏0
  • 乐字节Java8核心特性实战方法引用

    摘要:大家好,我是乐字节的小乐,上一次我们说到了核心特性之函数式接口,接下来我们继续了解又一核心特性方法引用。方法引用是一种更简洁易懂的表达式。感谢光临阅读小乐的,敬请关注乐字节后续将继续讲述等前沿知识技术。 大家好,我是乐字节的小乐,上一次我们说到了Java8核心特性之函数式接口,接下来我们继续了解Java8又一核心特性——方法引用。 showImg(https://segmentfaul...

    lakeside 评论0 收藏0
  • 乐字节-Java8核心特性实战函数式接口

    摘要:大家好,上一篇小乐给大家讲述了乐字节核心特性表达式,点击回顾。接下来继续核心特性之函数式接口。感谢大家欣赏小乐带来的核心特性之函数式接口,接下来还会更多核心技术讲解,请关注乐字节如需要视频课程,请搜索乐字节腾讯课堂 大家好,上一篇小乐给大家讲述了《乐字节-Java8核心特性-Lambda表达式》,点击回顾。接下来继续:Java8核心特性之函数式接口。 什么时候可以使用Lambda?通常...

    niceforbear 评论0 收藏0
  • 乐字节-Java8核心特性实战Lambda表达式

    摘要:使用表达式,使得应用变得简洁而紧凑。很多语言等从设计之初就支持表达式。表达式的参数与函数式接口内方法的参数,返回值类型相互对应。更多教程和资料请上腾讯课堂乐字节 showImg(https://segmentfault.com/img/bVbtotg?w=935&h=345); Java8 引入Lambda表达式,允许开发者将函数当成参数传递给某个方法,或者把代码本身当作数据进行处理。...

    Karuru 评论0 收藏0
  • Java学习必备书籍推荐终极版!

    摘要:实战高并发程序设计推荐豆瓣评分书的质量没的说,推荐大家好好看一下。推荐,豆瓣评分,人评价本书介绍了在编程中条极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。 很早就想把JavaGuide的书单更新一下了,昨晚加今天早上花了几个时间对之前的书单进行了分类和补充完善。虽是终极版,但一定还有很多不错的 Java 书籍我没有添加进去,会继续完善下去。希望这篇...

    Steve_Wang_ 评论0 收藏0

发表评论

0条评论

nemo

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<