Java 8 新特性

接口的默认方法

在 Java 8 中,我们可以通过default关键字来为接口添加非抽象方法。default关键字修饰的方法称为默认方法,它允许我们添加新的功能到现有库的接口中,并能确保与采用旧版本接口编写的代码之间相互兼容。
对于以下例子:

interface Formula {
    double calculate(int a);

    default double sqrt(int a) {
        return Math.sqrt(a);
    }
}

Formula接口中,除了有抽象方法calculate,还定义了默认方法sqrtFormula的实现类只需实现抽象方法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 以匿名对象的方式实现了Formula接口,而这只是为了实现sqrt(a * 100),略显繁琐的,在下一部分,将讨论一种在 Java 8 中更为优雅的实现方式。

Lambda表达式

首先让我们用 1.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 通过lambad 表达式来简化语法规则:

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

上面的代码更加精简,可读性也更强,当然,还可以继续精简:

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

Lambda 表达式的主体只有一条语句时,花括号{}和return关键字可省略。
现在,列表有了一个sort方法,另外,当可以从上下文推断出参数的类型,同样可以省略掉参数类型。

Lambad 表达式的结构

  • 一个 Lambda 表达式可以有零个或多个参数
  • 参数的类型既可以明确声明,也可以根据上下文来推断。例如:(int a)与(a)效果相同
  • 所有参数需包含在圆括号内,参数之间用逗号相隔。例如:(a, b) 或 (int a, int b) 或 (String a, int b, float c)
  • 空圆括号代表参数集为空。例如:() -> 42
  • 当只有一个参数,且其类型可推导时,圆括号()可省略。例如:a -> return a*a
  • Lambda 表达式的主体可包含零条或多条语句
  • 如果 Lambda 表达式的主体只有一条语句,花括号{}可省略。匿名函数的返回类型与该主体表达式一致
  • 如果 Lambda 表达式的主体包含一条以上语句,则表达式必须包含在花括号{}中(形成代码块)。匿名函数的返回类型与代码块的返回类型一致,若没有返回则为空

函数式接口

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注解,上述代码仍然是正确的。

方法和构造器引用

方法引用的分类:

  • 类名::静态方法名
  • 对象::实例方法名
  • 类名::实例方法名
  • 类名::new

类名::静态方法名

上述例子中的代码可以进一步通过静态方法引用来精简:

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"

类名::实例方法名

public class Person {
    private String name;

    public Person() {
    
    }

    public Student(String name){
        this.name = name;
    }

    public int compareByScore(Student student){
        return this.getScore() - student.getScore();
    }
}
students.sort(Student::compareByScore);
students.forEach(student -> System.out.println(student.getScore()));

sort 方法接收的 Lambda 表达式本该有两个参数,而这个实例方法只有一个参数也满足 Lambda 表达式的定义。这就是 类名::实例方法名 这种方法引用的特殊之处,当使用 类名::实例方法名 方法引用时,一定是 Lambda 表达式所接收的第一个参数来调用实例方法,如果 Lambda 表达式接收多个参数,其余的参数作为方法的参数传递进去。

类名::new

让我们来看看::关键字在构造器引用中是如何使用的。首先,我们定义一个有多个构造函数的类:

class Person {
    String firstName;
    String lastName;

    Person() {}

    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

接下来,我们创建一个用于创建新人员的工厂接口:

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 构造器传了一个引用(注:Person类中需要有无参构造器),Java编译器会自动选择正确的构造器。

Lambda作用域

Lambda 表达式访问外部变量的方式与匿名对象非常相似,它可以访问局部外围的 final 变量、成员变量和静态变量。

访问局部变量

我们可以在 Lambda 表达式所在的外部范围访问final修饰的局部变量

final int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3

不同于匿名对象的是,上述变量 num 不一定要被声明为 final(匿名内部类中的参数必须声明为 final,其值是 capture by value的),下述代码也是正确的:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3

值得注意的是,虽然 num 变量不需要显式声明为 final,但实际上,编译器要求 Lambda 表达式中捕获的变量必须实际上是最终变量(也就是初始化后不可再赋新值)所以 num 不可更改,下述代码无法通过编译,原因就是 num 的值被更改了:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);
num = 3;

访问成员变量和静态变量

与局部变量不同的是,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);

内置函数式接口

JDK 1.8 API 中包含了很多内置的函数式接口,其中一部分例如ComparatorRunnable在之前的 JDK 版本中就被人熟知。这些现有的接口通过@FunctionalInterface注解被拓展来支持 Lambda。
Java 8中的 API 也提供了一些新的函数式接口来使得编程更加简单。
以下是常用的函数式接口

函数式接口 参数类型 返回类型 抽象方法名 描述 其他方法
Runnable void run 作为无参数或返回值的动作运行  
Supplier T get 提供一个T类型的值  
Consumer T void accept 处理一个T类型的值 andThen
BiConsumer T,U void accept 处理T和U类型的值 andThen
Function T R apply 有一个T类型参数的函数 compose,andThen,identity
BiFunction T,U R apply 有T和U类型参数的函数 andThen
UnaryOperator T T apply 类型T上的一元操作符 compose,andThen,identity
BinaryOperator T,T T apply 类型T上的二元操作符 andThen,maxBy,minBy
Predicate T boolean test 布尔值函数 and,or,negate,isEqual
BiPredicate T,U boolean test 有两个参数的布尔值函数 and,or,negate

Predicate

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();

Function

Function接受一个参数并且返回一个结果,可以使用默认方法(compose,andThen)将多个函数链接起来。

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);

backToString.apply("123");     // "123"

Supplier

Supplier返回一个给定类型的结果,与Function不同的是,Supplier不接受任何参数。

Supplier<Person> personSupplier = Person::new;
personSupplier.get();   // new Person

Consumer

Comsumer代表了在一个输入参数上需要进行的操作.

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

Comparator

Comparator在之前的 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

Optional

Optional并非是一个函数式接口,但却是一个精巧的工具接口,用来防止NullPointerException,这个概念对于下一部分显得很重要,所以我们在这快速浏览一下Optional是如何工作的。
Optional是一个简单的值容器,这个值可以是 null,也可以是 non-null 的。考虑一个方法可能返回一个 non-null 值的结果,也有可能返回一个空值。在 Java 8中,为了不直接返回 null,你可以返回一个Optional

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"

Streams

java.util.Stream代表了可以在其上面执行一个或多个操作的元素序列。流操作是中间或者完结操作。完结操作会返回一个某种类型的值,而中间操作会返回流本身,因此你可以连续链接多个方法的调用。Stream 是在一个源的基础上创建出来的,例如java.util.Collection中的 lists 或 sets(不支持 maps)。流操作可以被顺序或者并行执行。
让我们先来了解下序列流是如何工作的,首先,我们通过字符串列表的形式创建一个示例代码:

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()来创建流。接下来的部分将会解释最常用的流操作。

Filter

Filter 接受一个 predicate 类型的接口来过滤流中的元素。该操作是一个中间操作,因此它允许我们在返回结果的时候再调用其他流操作(forEach)。ForEach 接受一个 Consumer 类型的接口变量,用来执行对多虑的流中的每一个元素的操作。ForEach是一个完结操作,并且不返回流,因此我们不能再调用其他的流操作。

stringCollection
    .stream()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

// "aaa2", "aaa1"

Sorted

Sorted 是一个中间操作,其返回一个流排序后的视图,流中的元素默认按照自然顺序进行排序,除非你指定了一个Comparator接口来重定义排序规则。

stringCollection
    .stream()
    .sorted()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

// "aaa1", "aaa2"

需要注意的是,sorted只是创建了流排序后的视图,并没有操作操作集合,集合中元素的顺序是没有改变的。

System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

Map

中间操作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"

Match

有多种匹配操作可以用来检查某一种规则是否与流对象相匹配。所有的匹配操作都是完结操作,并且返回一个 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

Count 是一个完结操作,它返回一个 long 类型数值,用来标识流对象中包含的元素数量。

long startsWithB =
    stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();

System.out.println(startsWithB);    // 3

Reduce

这个完结操作通过给定的函数来对流元素进行削减操作,该缩减操作的结果保存在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"

Parallel Streams

正如上面提到的,stream 可以是顺序的也可以是并行的。顺序操作通过单线程执行,而并行操作通过多线程执行。
下面的例子说明了使用并行流提高运行效率是多么的容易。
首先我们创建一个包含不同元素的列表:

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

两个代码片段几乎一样,但是使用并行操作来排序的效率提高了接近一半,而你需要做得就仅是将stream替换为parallelStream

Map

正如前面提到的,map 是不支持流操作的。Map接口本身没有可用的stream()方法,但是你可以根据键-值对或项通过map.keySet().streammap.values().stream()map.entrySet().stream()来创建指定的流。
此外,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防止我们写入额外的空值检查,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.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 中,或者调用合并函数来更改现有值。

日期API

Java 8 在java.time包下包含了全新的日期和时间 API,这个新的日期 API 与 Joda-Time 库相似,但不完全一样。下面的例子涵盖了大部分新的 API。

Clock

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

Timezone 由一个ZoneId来表示,他们可以通过静态工厂方法获得。时区定义了某一时刻和当地日期、时间之间转换的偏移量。

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

LocalTime 表示了一个没有指定时区的时间,例如 10 p.m 或者 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

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 表示的是日期-时间。它将日期和时间组合成一个实例。LocalDateTime是不可变的,与 LocalTimeLocalDate工作原理类似。我们可以利用方法去获取日期时间中的某些字段值。

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

通过一个时区的附件信息可以转换为一个实例,这个实例很容易转为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。我们可以使用自定义模式来取代预定义的格式进行格式化。

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.NumberFormatDateTimeFormatter是不可变的并且是线程安全的。
了解更多有关日期格式化的信息可以参考这里

注解

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 {}

使用变体2,Java 编译器隐式地对@Hint进行设置,这对于通过反射来读取注解信息非常重要。

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)来读取。然后,更便利的方法是getAnnotationByType,它可以直接访问@Hint注解。
此外,Java 8 中关于注解的使用,其还拓展了两个新的目标:

@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}

http://www.fme4008.cn
http://www.wfg7038.cn
http://www.edt2696.cn
http://www.gxq9182.cn
http://www.wbd0565.cn
http://www.evl5793.cn
http://www.tuq2170.cn
http://www.cwt3115.cn
http://www.lia3012.cn
http://www.qfc6292.cn
http://www.vjp6092.cn
http://www.okr2737.cn
http://www.vwl4798.cn
http://www.bfn6408.cn
http://www.jvu4337.cn
http://www.fgm2878.cn
http://www.bdu1500.cn
http://www.tdy5267.cn
http://www.nyh2569.cn
http://www.ncs7961.cn
http://www.zth2084.cn
http://www.zur3790.cn
http://www.wkj3723.cn
http://www.wac3757.cn
http://www.xlc5825.cn
http://www.jyf8675.cn
http://www.mxv9381.cn
http://www.uwy6316.cn
http://www.dww9805.cn
http://www.blw0160.cn
http://www.fbw5398.cn
http://www.mcp2369.cn
http://www.pzq1104.cn
http://www.sky3484.cn
http://www.sfo7099.cn
http://www.xbx8533.cn
http://www.ncn6620.cn
http://www.llu9392.cn
http://www.eqs1683.cn
http://www.slg6006.cn
http://www.url9735.cn
http://www.ney3099.cn
http://www.vab8501.cn
http://www.vkp1919.cn
http://www.awt9604.cn
http://www.igq3204.cn
http://www.ayp2545.cn
http://www.pgn4393.cn
http://www.npd9270.cn
http://www.sfi6002.cn
http://www.nne5492.cn
http://www.yai2320.cn
http://www.ics4171.cn
http://www.sfv9051.cn
http://www.ccg1407.cn
http://www.hjn1455.cn
http://www.ygf6271.cn
http://www.vfm1218.cn

猜你喜欢

转载自blog.csdn.net/shunfa888/article/details/80003216