Java 8 简明教程

Java 8 简明教程

“Java is still not dead–and people are starting to fingure that out”.

接口默认方法

  • java 8 可以在接口中使用default定义默认方法。可以多个
  • example:
    1
    2
    3
    4
    5
    6
    7
    interface Formula {
    double calculate(int a);
    default double sqrt(int a) {
    return Math.sqrt(a);
    }
    }

Lambda 表达式

  • 数组排序
  • 老的方式
1
2
3
4
5
6
7
8
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);
}
});
  • java 8 Lambda
1
2
3
Collections.sort(names, (String a, String b) -> {
return b.compareTo(a);
});
  • 进一步简化
1
Collections.sort(names, (String a, String b) -> b.compareTo(a));
  • 再简化,省略参数类型。编译器会自动识别参数类型
1
names.sort((a, b) -> b.compareTo(a));

函数接口

  • 一个Lamda表达式对应于一个函数接口中的抽象方法。
  • 一个函数接口有且只有一个抽象方法。
  • @FunctionalInterface(省略不报错)注解告诉编译器这是函数接口,以保证只有一个抽象方法。
  • Example:
1
2
3
4
@FunctionalInterface
interface Converter<F, T> {
T convert(F from);
}
1
2
3
Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted); // 123

方法和构造函数引用

  • java 8 允许使用::关键字引用对象的方法和构造函数
  • 引用静态方法

    1
    2
    3
    Converter<String, Integer> converter = Integer::valueOf;
    Integer converted = converter.convert("123");
    System.out.println(converted); // 123
  • 引用非静态方法

    1
    2
    3
    4
    5
    class Something {
    String startsWith(String s) {
    return String.valueOf(s.charAt(0));
    }
    }
1
2
3
4
Something something = new Something();
Converter<String, String> converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted); // "J"
  • 引用构造函数
1
2
3
4
5
6
7
8
9
10
11
class Person {
String firstName;
String lastName;
Person() {}
Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
  • Java编译器会根据函数接口方法的参数,自动选择符合声明的构造方法。
    1
    2
    3
    interface PersonFactory<P extends Person> {
    P create(String firstName, String lastName);
    }
1
2
PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");

Lambda 表达式的范围

与匿名内部类类似, 你可以在Lambda表达式中访问final局部变量实例变量以及静态变量

访问局部变量

1
2
3
4
5
final int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
  • 与匿名内部类不同的是,被Lambda表达式使用的变量默认为final,可以省略final。
1
2
3
4
5
int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
  • num默认final,以下代码num = 3会编译错误。
1
2
3
4
int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
num = 3;

访问实例变量和静态变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
};
}
}

访问函数接口默认方法

  • 默认方法不能通过Lambda表达式调用。
    1
    Formula formula = (a) -> sqrt(a * 100);//错误

Java 8 内置函数接口

  • 旧接口升级成函数接口

    • Comparator
    • Runnable
  • 类似 Google Guava library.

Predicate (判断)

  • Predicate 接受一个参数,返回boolean类型。
  • 定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @FunctionalInterface
    public interface Predicate<T> {
    /**
    * Evaluates this predicate on the given argument.
    *
    * @param t the input argument
    * @return {@code true} if the input argument matches the predicate,
    * otherwise {@code false}
    */
    boolean test(T t);
    ...
  • example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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 (函数)

  • Functions 接受一个参数,返回一个处理后的参数。
  • 定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @FunctionalInterface
    public interface Function<T, R> {
    /**
    * Applies this function to the given argument.
    *
    * @param t the function argument
    * @return the function result
    */
    R apply(T t);
    ...
  • example

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

Suppliers (生产者)

  • Suppliers 不接受参数,返回一个对象。
  • 定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @FunctionalInterface
    public interface Supplier<T> {
    /**
    * Gets a result.
    *
    * @return a result
    */
    T get();
    }
  • example

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

Consumers (消费者)

  • Consumers 接受一个参数,不返回。
  • 定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @FunctionalInterface
    public interface Consumer<T> {
    /**
    * Performs this operation on the given argument.
    *
    * @param t the input argument
    */
    void accept(T t);
    ...
  • example

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

Comparator (比较器)

  • java 8 增加了默认方法
  • 定义

    1
    2
    3
    4
    5
    @FunctionalInterface
    public interface Comparator<T> {
    int compare(T o1, T o2);
    ...
  • example

    1
    2
    3
    4
    5
    6
    7
    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

  • Optionals 可以避免 NullPointerException.
  • Optional是一个容器,空或者非空。
    example
1
2
3
4
5
6
7
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 可以执行一系列的流操作。
  • java.util.Collection like lists or sets (maps are not supported) 可以转化成Stream.
  • Stream 可以并行执行。

stream

1
2
3
4
5
6
7
8
9
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");

Filter

  • Filter 接受一个Predicate,根据Predicate过滤集合,这是一个中间操作。
1
2
3
4
5
6
stringCollection
.stream()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
// "aaa2", "aaa1"

Sorted

  • Sorted 是中间操作,默认按自然顺序排序,可以接受一个Comparator
  • Sorted 不会影响原来集合的排序。
1
2
3
4
5
6
7
stringCollection
.stream()
.sorted()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
// "aaa1", "aaa2"
1
2
System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

Map

  • Map 是中间操作,接受一个Function,一对一的映射。
1
2
3
4
5
6
7
stringCollection
.stream()
.map(String::toUpperCase)
.sorted((a, b) -> b.compareTo(a))
.forEach(System.out::println);
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

Match

  • Match是一个终止操作,接受Predicate,返回boolean。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
1
2
3
4
5
6
7
long startsWithB =
stringCollection
.stream()
.filter((s) -> s.startsWith("b"))
.count();
System.out.println(startsWithB); // 3

Reduce

  • Reduce是终止操作,接受BiFunction。返回Optional
1
2
3
4
5
6
7
8
Optional<String> reduced =
stringCollection
.stream()
.sorted()
.reduce((s1, s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

Collect

  • 将流转成集合的终止操作,Collectors.toList(), Collectors.toSet().

    1
    2
    3
    4
    5
    6
    7
    List<Person> filtered =
    persons
    .stream()
    .filter(p -> p.name.startsWith("P"))
    .collect(Collectors.toList());
    System.out.println(filtered); // [Peter, Pamela]
  • 分组 Collectors.groupingBy()

    1
    2
    3
    Map<Integer, List<Person>> personsByAge = persons
    .stream()
    .collect(Collectors.groupingBy(p -> p.age));
  • Collections.summaizeingInt()

    1
    2
    3
    4
    5
    6
    7
    IntSummaryStatistics ageSummary =
    persons
    .stream()
    .collect(Collectors.summarizingInt(p -> p.age));
    System.out.println(ageSummary);
    // IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}
  • Collections.joining();

    1
    2
    3
    4
    5
    String phrase = persons
    .stream()
    .filter(p -> p.age >= 18)
    .map(p -> p.name)
    .collect(Collectors.joining(" and ", "In Germany ", " are of legal age."));
  • Collection.toMap()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Map<Integer, String> map = persons
    .stream()
    .collect(Collectors.toMap(
    p -> p.age,
    p -> p.name,
    (name1, name2) -> name1 + ";" + name2));
    System.out.println(map);
    // {18=Max, 23=Peter;Pamela, 12=David}
  • 自定义Collection

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Collector<Person, StringJoiner, String> personNameCollector =
    Collector.of(
    () -> new StringJoiner(" | "), // supplier
    (j, p) -> j.add(p.name.toUpperCase()), // accumulator
    (j1, j2) -> j1.merge(j2), // combiner
    StringJoiner::toString); // finisher
    String names = persons
    .stream()
    .collect(personNameCollector);
    System.out.println(names); // MAX | PETER | PAMELA | DAVID

FlatMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class Streams7 {
static class Foo {
String name;
List<Bar> bars = new ArrayList<>();
Foo(String name) {
this.name = name;
}
}
static class Bar {
String name;
Bar(String name) {
this.name = name;
}
}
public static void main(String[] args) {
// test1();
test2();
}
static void test2() {
IntStream.range(1, 4)
.mapToObj(num -> new Foo("Foo" + num))
.peek(f -> IntStream.range(1, 4)
.mapToObj(num -> new Bar("Bar" + num + " <- " + f.name))
.forEach(f.bars::add))
.flatMap(f -> f.bars.stream())
.forEach(b -> System.out.println(b.name));
}
static void test1() {
List<Foo> foos = new ArrayList<>();
IntStream
.range(1, 4)
.forEach(num -> foos.add(new Foo("Foo" + num)));
foos.forEach(f ->
IntStream
.range(1, 4)
.forEach(num -> f.bars.add(new Bar("Bar" + num + " <- " + f.name))));
foos.stream()
.flatMap(f -> f.bars.stream())
.forEach(b -> System.out.println(b.name));
}
}
  • Optinal的flatMap使用
    1
    2
    3
    4
    5
    Optional.of(new Outer())
    .flatMap(o -> Optional.ofNullable(o.nested))
    .flatMap(n -> Optional.ofNullable(n.inner))
    .flatMap(i -> Optional.ofNullable(i.foo))
    .ifPresent(System.out::println);

Stream 进阶

其他的Stream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println); // a1
IntStream.range(1, 4)
.forEach(System.out::println);
// 1
// 2
// 3
// int array to stream
Arrays.stream(new int[] {1, 2, 3})
.map(n -> 2 * n + 1)
.average()
.ifPresent(System.out::println); // 5.0
Stream.of(1.0, 2.0, 3.0)
.mapToInt(Double::intValue)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
// a1
// a2
// a3

Stream处理顺序

  • 以下代码不会打印,因为没有终止操作。只有终止操作存在的时候,中间操作才虎执行。

    1
    2
    3
    4
    5
    Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
    System.out.println("filter: " + s);
    return true;
    });
  • 只要anymatch满足,就不会继续执行别的元素。由此可见,第一个元素走完全部,才会执行后面的元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Stream.of("d2", "a2", "b1", "b3", "c")
    .map(s -> {
    System.out.println("map: " + s);
    return s.toUpperCase();
    })
    .anyMatch(s -> {
    System.out.println("anyMatch: " + s);
    return s.startsWith("A");
    });
    // map: d2
    // anyMatch: D2
    // map: a2
    // anyMatch: A2
  • Stream的处理顺序很重要,尽量先筛选再处理。

Stream 重复使用

  • 一旦Stream终止,流就会关闭,不能再使用。
    1
    2
    3
    4
    5
    6
    Stream<String> stream =
    Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> s.startsWith("a"));
    stream.anyMatch(s -> true); // ok
    stream.noneMatch(s -> true); // exception
1
2
3
4
5
java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
at com.winterbe.java8.Streams5.test7(Streams5.java:38)
at com.winterbe.java8.Streams5.main(Streams5.java:28)

解决方法:用中间过程的流创建一个Supplier,返回Stream

1
2
3
4
5
6
Supplier<Stream<String>> streamSupplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));
streamSupplier.get().anyMatch(s -> true); // ok
streamSupplier.get().noneMatch(s -> true); // ok

Parallel Streams (并行流)

First we create a large list of unique elements:

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

Now we measure the time it takes to sort a stream of this collection.

Sequential Sort

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
8
9
10
11
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

As you can see both code snippets are almost identical but the parallel sort is roughly 50% faster. All you have to do is change stream() to parallelStream().

Maps

  • Map不能直接转化成Streammap.keySet().stream(), map.values().stream() and map.entrySet().stream().
  • Map
    1
    2
    3
    4
    5
    6
    7
    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));
1
2
3
4
5
6
7
8
9
10
11
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

Next, we learn how to remove entries for a given key, only if it’s currently mapped to a given value:

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

Another helpful method:

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

Merging entries of a map is quite easy:

1
2
3
4
5
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

Merge either put the key/value into the map if no entry for the key exists, or the merging function will be called to change the existing value.

Date API

Java 8 引入新的日期和时间接口, java.time.新的日期接口与Joda-Time library 兼容, however it’s not the same.

Clock

Clock 提供当前的时间和日期,是带时区的。 可以替代 System.currentTimeMillis()
Instant表示即使时间,可以转 java.util.Date objects.

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

Timezones

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
9
10
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
1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
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
1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
8
9
10
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
1
2
3
4
5
6
Instant instant = sylvester
.atZone(ZoneId.systemDefault())
.toInstant();
Date legacyDate = Date.from(instant);
System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014
1
2
3
4
5
6
7
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

Unlike java.text.NumberFormat the new DateTimeFormatter is immutable and thread-safe.

Annotations

Annotations in Java 8 are repeatable. Let’s dive directly into an example to figure that out.

First, we define a wrapper annotation which holds an array of the actual annotations:

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

Java 8 enables us to use multiple annotations of the same type by declaring the annotation @Repeatable.

Variant 1: Using the container annotation (old school)

1
2
@Hints({@Hint("hint1"), @Hint("hint2")})
class Person {}

Variant 2: Using repeatable annotations (new school)

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

Using variant 2 the java compiler implicitly sets up the @Hints annotation under the hood. That’s important for reading annotation information via reflection.

1
2
3
4
5
6
7
8
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

Although we never declared the @Hints annotation on the Person class, it’s still readable via getAnnotation(Hints.class). However, the more convenient method is getAnnotationsByType which grants direct access to all annotated @Hint annotations.

Furthermore the usage of annotations in Java 8 is expanded to two new targets:

1
2
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}
© 2017 Hello World All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero