“Java还活着-人们终于意识到了这一点."
欢迎阅读我对 Java 8 的介绍. 这篇教程会一步步的带领你领略它所有的语言新特性. 通过简短的实例代码你能学到如何使用默认接口方法, lambda表达式, 方法引用以及多重注解. 在文章的最后你会看到很多非常熟悉的 API 的改动如集合流, 函数性接口, map的扩展以及新的Date API.
废话不多说 - 上代码才是王道. 请君品尝!
“Java还活着-人们终于意识到了这一点."
欢迎阅读我对 Java 8 的介绍. 这篇教程会一步步的带领你领略它所有的语言新特性. 通过简短的实例代码你能学到如何使用默认接口方法, lambda表达式, 方法引用以及多重注解. 在文章的最后你会看到很多非常熟悉的 API 的改动如集合流, 函数性接口, map的扩展以及新的Date API.
废话不多说 - 上代码才是王道. 请君品尝!
Java 8 允许我们利用default
关键字给接口添加默认的方法实现. 这个特性也被成为扩展方法. 这里是我们的第一个例子:
interface Formula {
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}
在接口 Formula
中的抽象方法 calculate
的下面定义了默认方法sqrt
. 其实现类只需要实现抽象方法 calculate
. 默认方法 sqrt
可以被直接使用.
Formula formula = new Formula() {
@Override
public double calculate(int a) {
return sqrt(a * 100);
}
};
formula.calculate(100); // 100.0
formula.sqrt(16); // 4.0
formula是个实现了该接口的匿名内部类的对象 . 代码是相当的冗长: 整整6行代码只为了一个简单的计算 sqrt(a * 100)
. 下个章节中我们就会看到, Java 8中有更好的方式来实现只有一个方法的对象.
我们从一个简单的例子开始, 在之前的Java版本中, 是如何对字符串列表进行排序的:
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);
}
});
静态工具方法 Collections.sort
接收两个参数-一个列表对象和一个用来对这个列表进行排序的比较器对象. 你通常都会直接创建一个匿名内部类的比较器对象, 然后将它作为方法的参数.
为了代替这种创建匿名类对象的方式, Java 8提供了更为简短的语法 lambda 表达式:
Collections.sort(names, (String a, String b) -> {
return b.compareTo(a);
});
现在你可以看到代码变得更加简短. 但是这不够:
Collections.sort(names, (String a, String b) -> b.compareTo(a));
对于只有一行方法体的方法来说, 你可以省略方法体的那对 {}
以及 return
关键字. 但是这还不够:
Collections.sort(names, (a, b) -> b.compareTo(a));
那么lambda表达式是如何融入到Java类型系统中的呢? 其实每一个lambda都由一个接口指定对应到某一类型. 这个接口被称为函数接口 , 它只定义了一个抽象方法. 这种类型的所有的lambda表达式都和这个抽象方法相匹配. 默认方法不是抽象的, 所以你可以一个函数接口中定义任意多的默认方法.
只要接口只包含了一个抽象方法, 我们就可以通过lambda表达式的形式来使用它. 为了确保你的接口满足要求, 你必须给它添加@FunctionalInterface
注解. 编译器知道这个注解的含义, 而一旦你想往这个接口中添加其他的抽象方法时, 会出现编译错误.
示例:
@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
请注意就算这个接口上没有@FunctionalInterface
注解, 编译器也认为这是有效的.
利用静态方法的引用, 上面的示例代码可以进一步的简化:
Converter<String, Integer> converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted); // 123
Java 8 允许你通过关键字::
来传递方法或构造方法的引用. 上面的示例演示了如何引用一个静态方法. 同样我们也可以引用示例方法:
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 bean:
class Person {
String firstName;
String lastName;
Person() {}
Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
然后我们指定了一个工厂接口来创建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表达式中访问外部作用域中的变量的方式和匿名内部类的对象类似. 你可以访问外部的本地作用域中的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 = 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的匿名类对象来访问. 但是它不能在limbda表达式中使用.
默认方法不能通过lambda表达式来访问. 下列的代码不能编译通过:
Formula formula = (a) -> sqrt( a * 100);
JDK 1.8 API内置了大量函数接口. 其中有一些是大家耳熟能详的例如 Comparator
或者Runnable
. 这些已经存在的接口都通过注解 @FunctionalInterface
开启了Lambda支持.
当然Java 8 API同样提供了大量的新的函数接口来帮助你活的更轻松点. 有一些新的接口也很有名, 它们来源于 Google Guava 库. 不过即使你熟悉这个库, 也应该睁大双眼来观察这些接口是怎么扩展的.
Predicate是一个只有一个参数返回布尔值的函数接口. 这个接口包含各种默认的方法用来创建复杂逻辑计算(与或非)的断言.
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接收一个参数然后产生一个结果. 默认方法可以用来把多个函数连在一起 (compose, andThen).
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 在老版本Java中比较有名. Java 8 中在这个接口中新增了各种默认方法.
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
的极好工具. 它是下一节的重要内容, 我们在这里快速的浏览一下它是如何工作的.
Optional 是一个持有有可能为null的值的简单的容器. 有的方法可能会返回值但有时什么也不返回. 在Java 8中使用 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
列表或set集(不支持map). 集合流的操作支持顺序执行和并行执行.
让我们先看看顺序流是如何工作的. 首先我们写一段代码来创建一个字符串列表:
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");
Java 8 中的一些集合类已经实现了这个接口, 你可以很简单的通过调用Collection.stream()
或 Collection.parallelStream()
来创建一个集合流. 我们在下面的章节中解释一些很通用的集合流操作.
过滤方法接收一个断言对象(predicate)来过滤集合流中所有的元素. 这个操作是一个中间操作, 它允许我们在它之后调用另外一个集合流操作(forEach
). 遍历方法(ForEach) 会指定一个针对集合流中每个元素(已过滤)都会被调用的函数. 遍历方法是个一次性操作. 它返回void
, 所以我们就不能再调用其他方法了.
stringCollection
.stream()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
// "aaa2", "aaa1"
排序方法是一个中间方法, 它的返回值是集合流对象完成了排序后的视图. 默认是按照自然顺序来排序, 除非你指定了一个自定义的 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
通过指定的函数将每个元素转换成其他对象. 下面的例子将每个字符串都转换成大写. 当然你同样可以使用map
来将每个对象转换成其他类型的对象. 所产生的的集合流的泛化类型取决于你为 map
指定的函数的泛化类型.
stringCollection
.stream()
.map(String::toUpperCase)
.sorted((a, b) -> b.compareTo(a))
.forEach(System.out::println);
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
匹配方法是用来检查集合流是否满足一个特定的断言(predicate). 所有的这些操作都是一次性的, 返回的结果都是布尔值.
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
计数方法是一个一次性操作, 它返回 long
类型的数字, 来表示集合流中元素的个数
long startsWithB =
stringCollection
.stream()
.filter((s) -> s.startsWith("b"))
.count();
System.out.println(startsWithB); // 3
针对集合流的元素, 使用指定的函数规约的一次性操作. 结果是一个持有规约值的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());
}
现在我们记录一下时间, 对它进行排序
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
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
正如你所看到的, 这两个代码片段几乎是相同的, 但是并行排序快了大约 50% . 而你只需要将 stream()
改成 parallelStream()
.
上面提到了Map并不支持集合流. 但是Map现在新增了许多有用的方法来支持一些通用操作.
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
遍历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.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, 如果这个元素存在就会调用更新方法来改变原来的值.
Java 8 包含了一个全新的日期和时间的API, 在 java.time
包下. 新的日期 API可以和 Joda-Time 库相媲美, 不过它们并不一样. 下面的例子涉及到了新API中最重要的部分.
Clock 提供了对当前日期和时间的访问能力. Clocks 知道时区, 可以用来替代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]
LocalTime(本地时间)用来表示一个脱离了时区的时间, 例如. 10pm 或 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(本地日期) 表示一个确定的日期, 例如. 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类似. 我们可以利用它提供的方法来检索一个日期时间中的特定字段:
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
一个timezone的附加信息也可以转换为一个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-times和对dates和times的格式化类似. 我们可以创建一个自定义的转换器来取代预定义的准换格式.
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
是不可变的并且是线程安全的.
更多细节和语法特性请阅读这里.
Java 8支持多重注解了. 我们直接来看个例子就明白了.
首先, 我们定义了一个包含了实际注解数组的包装注解:
@interface Hints {
Hint[] value();
}
@Repeatable(Hints.class)
@interface Hint {
String value();
}
Java 8 允许我们在同一个类型上多次使用同一个注解, 只要这个注解声明为@Repeatable
.
@Hints({@Hint("hint1"), @Hint("hint2")})
class Person {}
@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(Hints.class)
获取到. 不过, 更为方便的方法是getAnnotationsByType
, 它可以直接获取到所有的 @Hint
注解.
另外, Java 8中的多了两种能注解的目标:
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}
我的Java 8教程到这里就结束了. 但是还有非常多的特性需要去挖掘. 有兴趣的话你可以自己来看看JDK 1.8中到底还有什么牛X闪闪的玩意, 例如.Arrays.parallelSort
,StampedLock
以及CompletableFuture
-这里只是顺便说一句而已.
希望本教程对你有所帮助. 完整的示例代码在 hosted on GitHub. 大家可以自由的 fork the repository 或者给我一些反馈Twitter.
翻译自:http://winterbe.com/posts/2014/03/16/java-8-tutorial/
原作者:Benjamin
译者: 老男孩
相关推荐
Java 8 教程.zip,Java 8 for all of us
JAVA 教程
java8 简介,语法和函数等简单用法,具体可参考文档中的示例,体会版本更新带来的好处。
Java8简明教程.pdf
Java 8编程入门官方教程
Java8简明教程、数据流教程、Nashorn教程、线程和执行器、同步和锁、原子变量和ConcurrentMap、字符串、数值、算术和文件、避免null检查、使用 Intellij IDEA 解决 Java 8 的数据流问题、在 Nashron 中使用 Backbone...
Java 8 简明教程
java8 官方教程书籍,讲解的比较基础,适合初学者。也可以配合java8 文档进行学习。
java8官方教程
java8的官方文档,全书1200多页,完整介绍了java语言的特性,并引入的java8新的语言特性,例如stream,lambda表达式等,适合作为工具书查阅,java8的官方文档,全书1200多页,完整介绍了java语言的特性,并引入的...
【完整课程列表】 完整版 Java基础教程 Java入门教程 Java语言程序设计 第1章 认识Java(共61页).ppt 完整版 Java基础教程 Java...完整版 Java基础教程 Java入门教程 Java语言程序设计 第8章 异常处理(共33页).ppt
Java8 教程 引言 这个 java8 教程列表中将分享那些 java8 中最重要的那些功能和链接。原文地址 如果觉得这些内容可以帮助到你,也可以在 上点一个:thumbs_up:支持一下~ 阅读地址 参与翻译 感谢以下译者 @...
原文:java8-tutorial Modern Java - A Guide to Java 8 http://winterbe.com ---------------------------------------------------- 本 ePub 基于开源文档,目录书签齐全。 版权归原作者,翻译版权归译者。 ------...
Java 8 简明教程 Java 8 Tutorial中文版 “Java并没有没落,人们很快就会发现这一点” 欢迎阅读我编写的Java 8介绍。本教程将带领你一步一步地认识这门语言的新特 性。通过简单明了的代码示例,你将会学习到如何使用...
尚硅谷Java视频Java8新特性视频教程尚硅谷Java视频Java8新特性视频教程尚硅谷Java视频Java8新特性视频教程
【完整课程列表】 完整版 Java初级教程 Java语言程序设计 第1章 Java语言简介(共16页).ppt 完整版 Java初级教程 Java语言程序设计 第2章 基本...完整版 Java初级教程 Java语言程序设计 第8章 集合框架(共19页).ppt
【完整课程列表】 完整版 Java高级教程 Java语言程序设计 第1章 输入输出流(共42页).ppt 完整版 Java高级教程 Java语言程序设计 第2章 Java多线程...完整版 Java高级教程 Java语言程序设计 第8章 JDBC(共24页).ppt
第8章 建立对话框 第9章 Java多线程机制 第10章 输入输出流 第11章 Java网络的基本知识 第12章 JavaApplet基础 第13章 常见数据结构的Java实现 第14章图形与图像 第15章 Java数据库连接(JDBC) 第16章 Java与多媒体 ...
【完整课程列表】 完整版 Java高级教程 Java语言程序设计 第1章 输入输出流(共42页).ppt 完整版 Java高级教程 Java语言程序设计 第2章 Java多线程...完整版 Java高级教程 Java语言程序设计 第8章 JDBC(共24页).ppt