IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Java 8 简记 -> 正文阅读

[Java知识库]Java 8 简记

Lambda表达式

概念

Lambda 表达式,也可称为闭包,其本质是一个Java语法糖,可以使匿名方法的代码变的更加简洁紧凑。同时,Lambda 允许把函数作为参数传递进方法中,推动了 Java 中的函数化编程。

语法

// 如果主体包含了一条语句,就不需要使用大括号;主体会将表达式的值作为返回值
() -> exp;

// 不需要声明参数类型
// 一个参数无需定义圆括号
p1 -> exp;

// 多个参数必须要定义圆括号
// 主体包含了多条语句,必须要使用大括号;大括号内需要显式指定返回值
(p1, p2) -> {
	exp1;
	exp2;
	return "xxx";
}

// lambda 表达式只能引用不被修改的外层局部变量,否则会编译错误
int a = 0; // 编译器会自动将该变量视为常量,不可修改。
myFun( () -> a + 2 );

函数式接口

  1. 函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法(静态方法或者默认方法)的接口。
  2. 函数式接口使用 @FunctionalInterface 注解。
  3. 函数式接口可以被隐式转换为 lambda 表达式,即可以作为参数直接传递给方法。
  4. 从 Java 8 开始,很多之前的接口,都被调整成函数式接口;也新增了很多新的函数式接口。
变成函数式接口的老接口
  1. Runnable
  2. Callable
  3. PrivilegedAction
  4. Comparator
  5. FileFilter
  6. PathMatcher
  7. InvocationHandler
  8. PropertyChangeListener
  9. ActionListener
  10. ChangeListener
Consumer接口

Consumer接口是消费性接口,对类型为T的对象应用操作,无返回值。

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);
    
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}
Supplier接口

Supplier接口是供给型接口,无输入,返回类型为T的对象。

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
Predicate接口

Predicate接口是断言型接口,确定类型为T的对象是否满足约束条件,返回值类型为boolean。

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}
Function接口

Function接口是函数型接口,对类型为T的对象应用操作,并返回R类型的返回结果。

@FunctionalInterface
public interface Function<T, R> {
    
    R apply(T t);
    
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}
新增的其他函数式接口
  1. BiFunction(T, U, R):对类型为T,U的参数应用操作,返回R类型的结果。
  2. UnaryOperator(Function子接口):对类型为T的对象进行一 元运算, 并返回T类型的 结果。
  3. BinaryOperator (BiFunction 子接口):对类型为T的对象进行二 元运算, 并返回T类型的 结果。
  4. BiConsumer<T, U>:对类型为T, U 参数应用 操作。
  5. ToIntFunction、ToLongFunction、ToDoubleFunction:计算 int 等类型值的函数
  6. IntFunction、LongFunction、DoubleFunction:参数为 int 等类型的函数

方法引用

方法引用只是简化Lambda表达式的一种手段,是用来直接访问类或者实例的已经存在的方法或者构造方法。

方法引用提供了一种引用而不执行方法的方式,会创建函数式接口的一个实例。

语法:

// 等价于:
// Arrays.sort(stringsArray,(s1,s2)->s1.compareToIgnoreCase(s2));
// 其中,Lambda表达式 (s1,s2)->s1.compareToIgnoreCase(s2) 只是执行一个已有方法的调用,可以使用方法引用来简化表达
Arrays.sort(stringsArray, String::compareToIgnoreCase);

// 静态方法引用
ClassName::staticMethodName

// 特定实例对象的方法引用
instance::methodName

// 类型上的实例方法引用
TypeName::methodName

// 构造方法的引用
ClassName::new
TypeName[]::new

Stream API

概念

Stream 是 Java8 中处理集合的关键抽象概念,它可以指定对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作,也可以并行执行操作。
它是一个函数式语言+多核时代综合影响的产物。

使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询,本质上是实现一个filter-map-reduce过程,是一种简洁、高效的集合数据处理方式。

特性

  1. Stream 不是数据结构,自己不会存储元素。
  2. Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
  3. Stream 可以是无限的。集合有固定大小,Stream 则不必。limit(n)和findFirst()这类的short-circuiting操作可以对无限的 Stream 进行运算并很快完成。
  4. Stream 不支持索引访问。你可以请求第一个元素,但无法请求第二个,第三个,或最后一个。
  5. 所有Stream的操作必须以lambda表达式为参数。
  6. 当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。
  7. Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行,例如:多个转换操作只会在Terminal操作的时候融合起来,一次循环完成。

操作步骤

Stream 的操作分为三个步骤

  1. 创建 Stream:一个数据源 (如 : 集合、数组), 获取一个流
  2. 中间操作(intermediate操作):多个个中间操作构成一个中间操作链,对数据源的数据进行处理
  3. 终止操作(Terminal操作):一个终止操作,执行中间操作链,并产生结果

并行流

并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
Stream API 可以声明性地通过 parallel() 与 sequential() 在并行流与顺序流之间进行切换。

创建 Stream

1)Java8 中的 Collection 接口被扩展,提供两个获取流的方法 :

  1. default Stream<E> stream() : 返回一个顺序流
  2. default Stream<E> parallelStream() : 返回一个并行流

2)Java8 中的 Arrays 的静态方法 stream() 可以获取数组流 :

  1. static <T> Stream<T> stream(T[] array) : 返回一个流

3)可以使用静态方法 Stream.of(), 通过显示值创建一个流,它可以接收任意数量的参数:

  1. public static<T> Stream<T> of(T... values) : 返回一个流

4)可以提供函数参数,使用静态方法 Stream.iterate() 和 Stream.generate(), 创建无限流:

  1. public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
  2. public static<T> Stream<T> generate(Supplier<T> s)

示例:

// Collection 提供了两个方法  stream() 与 parallelStream()
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream(); // 获取一个顺序流
Stream<String> parallelStream = list.parallelStream(); // 获取一个并行流

// 通过 Arrays 中的 stream() 获取一个数组流
Stream<Integer> stream1 = Arrays.stream ( new Integer[ 10 ]);
IntStream stream2 = Arrays.stream ( new int[ 10 ]);

// 通过 Stream 类中静态方法 of()
Stream<Integer> stream3 = Stream.of ( 1 , 2 , 3 , 4 , 5 , 6 );

// 创建无限流
// 迭代
Stream<Integer> stream3 = Stream.iterate ( 0 , (x) -> x + 2 ).limit( 10 );
// 生成
Stream<Double> stream4 = Stream.generate (Math:: random ).limit( 2 );

中间操作

惰性求值

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理。而在终止操作时一次性全部处理,称为“惰性求值”。

// 如下代码,只是定义stream,所有的中间操作不会做任何的处理
Stream<Employee> stream = emps.stream().filter((e) -> e.getAge() <= 35 );
// 如下代码,开始做终止操作时,所有的中间操作会一次性的全部执行,称为“惰性求值”
stream.forEach(System.out ::println);
筛选与切片
  1. filter(Predicate p):接收 Lambda , 从流中排除某些元素
  2. distinct():筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
  3. limit(long maxSize):截断流,使其元素不超过给定数量
  4. skip(long n):跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流
映射
  1. map(Function f):接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素
  2. mapToDouble(ToDoubleFunction f)、mapToInt(ToIntFunction f)、mapToLong(ToLongFunction f):接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream 等
  3. flatMap(Function f):接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流

示例:

// 使用map()来转换为大写
List<String> strList = Arrays.asList ( "aaa" , "bbb" , "ccc" , "ddd" , "eee" );
Stream<String> stream1 = strList.stream().map(String::toUpperCase);
stream1.forEach(System.out ::println);

// 使用flatMap()来把多个单词拆分成字母序列
public static Stream<Character> filterCharacter(String str) {
	// 该函数将单个单词拆分成字母流
    List<Character> list = new ArrayList<>();
    for (Character ch : str.toCharArray()) {
        list.add(ch);
    }
    return list.stream();
}

Stream<Character> stream3 = strList.stream().flatMap((str) -> filterCharacter(str));
stream3.forEach(System.out::println);

// 使用map()来转换为平方数
Integer[] nums = new Integer[]{ 1 , 2 , 3 , 4 , 5 };
Arrays.stream (nums).map((x) -> x * x).forEach(System.out ::println);
排序
  1. sorted():产生一个新流,其中按自然顺序排序
  2. sorted(Comparator comp):产生一个新流,其中按比较器顺序排序

示例:

List<Integer> list = new ArrayList<>();
list.stream().sorted((x, y) -> {
            return x > y;
        }).forEach(System.out ::println);

终止操作

终止操作后流不能再使用

终端操作会从流的流水线生成结果,其结果可以是任何不是流的值,例如 : List、 Integer,甚至是 void。故,流进行了终止操作后,不能再次使用。

// 定义stream
Stream<Employee> stream = emps.stream().filter((e) -> e.getAge() <= 35 );
// 执行终止操作
stream.forEach(System.out ::println);
// 流已经不可再使用,如下代码会报错!
// stream.limit(5).count();
Optional 类

Optional 仅仅是一个包装类:存放T类型的值或者null,它提供了一些有用的接口来避免显式的null检查。

  1. Optional.of(T t) : 创建一个 Optional 实例
  2. isPresent() : 判断是否包含值
  3. orElse(T t) : 如果调用对象包含值,返回该值,否则返回t
  4. Optional.empty() : 创建一个空的 Optional 实例
  5. Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例
  6. map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()

详见:https://www.jianshu.com/p/d81a5f7c9c4e

查找与匹配
  1. allMatch(Predicate p):检查是否匹配所有元素
  2. anyMatch(Predicate p):检查是否至少匹配一个元素
  3. noneMatch(Predicate p):检查是否没有匹配所有元素
  4. findFirst():返回第一个元素
  5. findAny():返回当前流中的任意元素
  6. count():返回流中元素总数
  7. max(Comparator c):返回流中最大值
  8. min(Comparator c):返回流中最小值
  9. forEach(Consumer c):内部迭代(与之相对的:使用 Collection 接口需要用户去做迭代,称为外部迭代。)
归约
  1. reduce(T iden, BinaryOperator b):可以将流中元素反复结合起来,得到一个值,返回 T
  2. reduce(BinaryOperator b):可以将流中元素反复结合起来,得到一个值,返回 Optional<T>

示例:

// 将数组的所有值相加求和
List<Integer> list = Arrays.asList ( 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 );
Integer sum = list.stream().reduce( 0 , (x, y) -> x + y);
收集
  1. collect(Collector c):将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法

示例:

// Collector 接口中方法的实现决定如何对流执行收集操作(如收集到 List、 Set、 Map)
List<String> list = emps.stream().map(Employee::getName).collect(Collectors.toList ());
Set<String> set = emps.stream().map(Employee::getName).collect(Collectors.toSet ());
HashSet<String> hs = emps.stream().map(Employee::getName).collect(Collectors.toCollection (HashSet:: new ));

Optional<Double> max = emps.stream().map(Employee::getSalary).collect(Collectors.maxBy (Double:: compare ));
Optional<Employee> min = emps.stream().collect(Collectors.minBy ((e1, e2) -> Double.compare (e1.getSalary(), e2.getSalary())));
Double sum = emps.stream().collect(Collectors.summingDouble (Employee::getSalary));
Double avg = emps.stream().collect(Collectors.averagingDouble (Employee::getSalary));
Long count = emps.stream().collect(Collectors.counting ());
DoubleSummaryStatistics dss = emps.stream().collect(Collectors.summarizingDouble (Employee::getSalary)); // 收集统计值,DoubleSummaryStatistics中含有各种统计值,如总和、最小值、最大值、平均值等

Map<Status, List<Employee>> map = emps.stream().collect(Collectors.groupingBy (Employee::getStatus)); // 根据某属性值对流分组,得到以属性值为key的map
Map<Status, Map<String, List<Employee>>> map = emps.stream().collect(Collectors.groupingBy (Employee::getStatus, Collectors.groupingBy ((e) -> {
            if (e.getAge() >= 60 ) {
                return "老年" ;
            } else if (e.getAge() >= 35 ) {
                return "中年" ;
            } else {
                return "成年" ;
            }
        }))); // 多级分组
Map<Boolean,List<Emp>> vd = list.stream().collect(Collectors.partitioningBy(Employee::getManage)); // 根据true或false进行分区

String str = emps.stream().map(Employee::getName).collect(Collectors.joining ( "," , "----" , "----" )); // 连接流中每个字符串,其参数均为可选,依次为:拼接字符,前缀,后缀

Optional<Double> sum = emps.stream().map(Employee::getSalary).collect(Collectors.reducing (Double:: sum )); // 从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值
int total = list.stream().collect(Collectors.reducing(0, Employee::getSalar, Integer::sum));

int listSize = list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size)); // 包裹另一个收集器,对其结果调用转换函数进行处理

其他语言新特性

接口支持默认方法和静态方法

所谓的接口默认方法,就是指接口中可以使用 default 关键字定义非抽象方法,来为该方法提供默认实现,从而实现类可以使用默认实现也可以覆盖该方法。

接口可以实现静态方法。静态方法是接口的一部分,可以通过接口名来调用,但是不能被实现类进行覆盖。

这个特性对重构很友好:修改接口后不需要大范围的修改以前老的实现类。

举例:

public interface MyInterface {     
	// 静态变量   
	static final String  staticVar = "";

	// 静态方法
	static String myStaticFun2() {
		return staticVar;
	}

	// 默认方法
	default String introduce() {
		return "";
	}
}
多个同名默认方法

一个类实现了多个接口,且这些接口有同名的默认方法。可以使用 super 来调用指定接口的默认方法:InterfaceName.super.methodName();

支持重复注解,增加注解的使用场景

Java 8 之前,注解有一个很大的限制是:在同一个地方不能多次使用同一个注解。Java 8打破了这个限制,引入了重复注解的概念,允许在同一个地方多次使用同一个注解:使用 @Repeatable 定义重复注解。

ElementType.TYPE_USERElementType.TYPE_PARAMETER 是 Java 8 新增的两个注解,用于描述注解的使用场景。
Java 8 新增了很多注解的使用场景,具体支持哪些使用场景,建议查看 ElementType 这个类。

其他官方类库新特性

时间API

LocalDate、 LocalTime、 LocalDateTime

LocalDate、 LocalTime、 LocalDateTime 类的实例是不可变的对象(每次都需要创建新的实例),分别表示使用 ISO-8601日历系统的日期、时间、日期和时间,也不包含与时区相关的信息。

// 获取当前系统时间
LocalDateTime localDateTime1 = LocalDateTime.now();
// 指定日期时间
LocalDateTime localDateTime2 = LocalDateTime.of(2019, 10, 27, 13, 45,10);

// 对日期进行加减
LocalDateTime localDateTime3 = localDateTime1.plusYears(3).minusMonths(3);

// 获取日期的年月日
localDateTime1.getYear();
localDateTime1.getMonthValue();
localDateTime1.getDayOfMonth();
localDateTime1.getHour();
localDateTime1.getMinute();
localDateTime1.getSecond();
Instant

用于“时间戳”的运算。它是以Unix元年(传统的设定为UTC时区1970年1月1日午夜时分)开始所经历的秒数进行运算。

// 默认获取UTC时区
Instant instant1 = Instant.now();
// 偏移量运算
OffsetDateTime offsetDateTime = instant1.atOffset(ZoneOffset.ofHours(8));
ZonedDateTime zonedDateTime = instant1.atZone(ZoneId.systemDefault());
// 获取时间戳
instant1.toEpochMilli();
instant1.getEpochSecond();
// 以Unix元年为起点,进行偏移量运算
Instant instant2 = Instant.ofEpochMilli(1584700633222L);
Instant instant3 = Instant.ofEpochSecond(60);
DateTimeFormatter

对日期进行解析与格式化。

// 预定义的标准格式,如下示例为yyyy-MM-dd
DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ISO_DATE;
LocalDateTime localDateTime = LocalDateTime.now();
String strDate1 = localDateTime.format(dateTimeFormatter1);
// 自定义的格式
DateTimeFormatter dateTimeFormatter2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String strDate2 = dateTimeFormatter2.format(localDateTime);

// 解析日期
LocalDateTime localDateTime1 = localDateTime.parse(strDate2, dateTimeFormatter2);
其他
  1. Duration:用于计算两个“时间”间隔。
  2. Period:用于计算两个“日期”间隔 。
  3. TemporalAdjuster:时间校正器。例如:将日期调整到“下个周日”等操作。
  4. ZonedDate、 ZonedTime、 ZonedDateTime:带时区的时间。
  5. ZoneId:该类中包含了所有的时区信息。
  6. Java 8之前的日期处理的“遗留类”,也新添加了方法来支持与新的时间API进行转换。例如:Date.from(instant); date.toInstant(); Date.valueOf(loacalDate);

Base64

// Base64编码
Base64.getEncoder().encodeToString(bytes); 
// Base64解码
Base64.getDecoder().decode(str); 
// URL编解码
Base64.getUrlEncoder() / Base64.getUrlDecoder()
// MINE编解码
Base64.getMimeEncoder() / Base64.getMimeDecoder()

数组并发处理

Java8版本新增了很多新的方法,用于支持并行数组处理。

比如 Arrays.parallelSort( array ),可以显著加快多核机器上的大数组排序。
因为如果数组大小小于或等于 8192,或者处理器只有一个核心,它将使用顺序的 Dual-Pivot Quicksort 算法,故此时性能并没有优势。

其他并发支持

Java 8 在并发方面做了一些改进:

  1. 为 ConcurrentHashMap 类添加了新的方法来支持聚焦操作;
  2. 为 ConcurrentForkJoinPool 类添加了新的方法来支持通用线程池操作;
  3. 添加了新的 StampedLock 类,用于支持基于容量的锁,是 ReadWriteLock 的替代者;
  4. java.util.concurrent.atomic 包中也新增了不少工具类,如:DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder。

Nashorn JavaScript引擎

Java 8 提供了新的Nashorn JavaScript引擎,使得我们可以在JVM上开发和运行JS应用,允许Java和JavaScript交互使用。
注:Nashorn JavaScript Engine 在 Java 15 已经不可用了。

      ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
      ScriptEngine nashorn = scriptEngineManager.getEngineByName("nashorn");
      String js = "10 + 2";
      Integer result = (Integer) nashorn.eval( js );

其他命令行工具/编译器等新特性

Nashorn引擎:jjs

jjs是一个基于标准Nashorn引擎的命令行工具,可以接受js源码文件并执行:jjs func.js

类依赖分析器:jdeps

jdeps可以展示包层级和类层级的Java类依赖关系,它以.class文件、目录或者Jar文件为输入,然后会把依赖关系输出到控制台。
例如:jdeps xxx.classjdeps xxx.jar

获取方法的参数名称:-parameters参数

为了在运行时获得Java程序中方法的参数名称,Java 8 在语言层面(使用反射API和Parameter.getName()方法)和字节码层面(使用新的javac编译器以及 -parameters 参数)提供支持。

在 Java 8 中这个特性是默认关闭的,可以在pom文件中通过 maven-compiler-plugin 来进行配置开启。

JVM的新特性

  1. 使用Metaspace(JEP 122)代替持久代(PermGen space);
  2. JVM参数,使用-XX:MetaSpaceSize和-XX:MaxMetaspaceSize代替原来的-XX:PermSize和-XX:MaxPermSize;
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-04-18 17:27:33  更:2022-04-18 17:28:22 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 4:25:56-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码