深入学习java源码之lambda表达式与函数式接口

深入学习java源码之lambda表达式与函数式接口

@FunctionalInterface
JDK中的函数式接口举例
java.lang.Runnable,
java.awt.event.ActionListener, 
java.util.Comparator,
java.util.concurrent.Callable
java.util.function包下的接口,如Consumer、Predicate、Supplier等
所谓的函数式接口,当然首先是一个接口,然后就是在这个接口里面只能有一个抽象方法。
加不加@FunctionalInterface对于接口是不是函数式接口没有影响,该注解知识提醒编译器去检查该接口是否仅包含一个抽象方法
主要用在Lambda表达式和方法引用(实际上也可认为是Lambda表达式)上。
如定义了一个函数式接口如下:

   @FunctionalInterface
    interface GreetingService 
    {
        void sayMessage(String message);
    }

那么就可以使用Lambda表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的):

GreetingService greetService1 = message -> System.out.println("Hello " + message);	

Consumer< T>接口接受一个T类型参数,没有返回值。

package java.util.function;

import java.util.Objects;

@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); };
    }
}

进一步的,还可以这样使用:

class MyStream<T>{
    private List<T> list;
    ...
    public void myForEach(GreetingService <T> consumer){// 1
        for(T t : list){
            consumer.accept(t);
        }
    }
}
MyStream<String> stream = new MyStream<String>();
stream.myForEach(str -> System.out.println(str));// 使用自定义函数接口书写Lambda表达式

Consumer测试代码:

public void test(){
    UserT userT = new UserT("zm");
    //接受一个参数
    Consumer<UserT> userTConsumer = userT1 -> userT1.setName("zmChange");
    userTConsumer.accept(userT);
    logger.info(userT.getName());//输出zmChange
}

java8以前的实现如下:

public void test1(){
    UserT userT = new UserT("zm");
    this.change(userT);
    logger.info(userT.getName());//输出zmChange
}

private void change(UserT userT){
    userT.setName("zmChange");
}

list集合的遍历器

    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

 

Predicate和Consumer接口

package java.util.function;
import java.util.Objects;

@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);
    }
}

为了详细说明Predicate和Consumer接口,通过一个学生例子:
Student类包含姓名、分数以及待付费用,每个学生可根据分数获得不同程度的费用折扣。

public class Student {

    String firstName;
    String lastName;
    Double grade;
    Double feeDiscount = 0.0;
    Double baseFee = 2000.0;

    public Student(String firstName, String lastName, Double grade) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.grade = grade;
    }

    public void printFee(){
        Double newFee = baseFee - ((baseFee * feeDiscount)/100);
        System.out.println("The fee after discount: " + newFee);
    }
}

然后分别声明一个接受Student对象的Predicate接口以及Consumer接口的实现类。
本例子使用Predicate接口实现类的test()方法判断输入的Student对象是否拥有费用打折的资格,然后使用Consumer接口的实现类更新输入的Student对象的折扣。

public class PredicateConsumerDemo {
    public static Student updateStudentFee(Student student, Predicate<Student> predicate, Consumer<Student> consumer){
        if (predicate.test(student)){
            consumer.accept(student);
        }
        return student;
    }
}

Predicate和Consumer接口的test()和accept()方法都接受一个泛型参数。不同的是test()方法进行某些逻辑判断并返回一个boolean值,而accept()接受并改变某个对象的内部值。updateStudentFee方法的调用如下所示:

public class Test {
    public static void main(String[] args) {
        Student student1 = new Student("Ashok","Kumar", 9.5);

        student1 = updateStudentFee(student1,
                //Lambda expression for Predicate interface
                student -> student.grade > 8.5,
                //Lambda expression for Consumer inerface
                student -> student.feeDiscount = 30.0);
        student1.printFee(); //The fee after discount: 1400.0

        Student student2 = new Student("Rajat","Verma", 8.0);
        student2 = updateStudentFee(student2,
                //Lambda expression for Predicate interface
                student -> student.grade >= 8,
                //Lambda expression for Consumer inerface
                student -> student.feeDiscount = 20.0);
        student2.printFee();//The fee after discount: 1600.0

    }
}

java源码使用

    @Override
    public boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        // figure out which elements are to be removed
        // any exception thrown from the filter predicate at this stage
        // will leave the collection unmodified
        int removeCount = 0;
        final BitSet removeSet = new BitSet(size);
        final int expectedModCount = modCount;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            @SuppressWarnings("unchecked")
            final E element = (E) elementData[i];
            if (filter.test(element)) {
                removeSet.set(i);
                removeCount++;
            }
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }

        // shift surviving elements left over the spaces left by removed elements
        final boolean anyToRemove = removeCount > 0;
        if (anyToRemove) {
            final int newSize = size - removeCount;
            for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
                i = removeSet.nextClearBit(i);
                elementData[j] = elementData[i];
            }
            for (int k=newSize; k < size; k++) {
                elementData[k] = null;  // Let gc do its work
            }
            this.size = newSize;
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
            modCount++;
        }

        return anyToRemove;
    }

 

UnaryOperator接口

这个接口继承Function接口,Funtion接口,定义了一个apply的抽象类,接收一个泛型T对象,并且返回泛型R对象

package java.util.function;

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {

    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}

关于Funtcion的意思以及用法;


package java.util.function;

import java.util.Objects;

@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;
    }
}

这个接口,只接收一个泛型参数T,集成Function接口,也就是说,传入泛型T类型的参数,调用apply后,返回也T类型的参数;这个接口定义了一个静态方法,返回泛型对象的本身;

具体用法

UnaryOperator<Integer> dda = x -> x + 1;
System.out.println(dda.apply(10));// 11
UnaryOperator<String> ddb = x -> x + 1;
System.out.println(ddb.apply("aa"));// aa1

 

匿名内部类的使用

匿名内部类也就是没有名字的内部类正因为没有名字,所以匿名内部类只能使用一次,它通常用来简化代码编写,但使用匿名内部类还有个前提条件:必须继承一个父类或实现一个接口,但最多只能继承一个父类,或实现一个接口。
关于匿名内部类还有如下两条规则:
    1)匿名内部类不能是抽象类,因为系统在创建匿名内部类的时候,会立即创建内部类的对象。因此不允许将匿名内部类定义成抽象类。
    2)匿名内部类不等定义构造器(构造方法),因为匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义实例初始化块,
    怎样判断一个匿名类的存在啊?看不见名字,感觉只是父类new出一个对象而已,没有匿名类的名字。

abstract class Father(){
....
}
public class Test{
   Father f1 = new Father(){ .... }  //这里就是有个匿名内部类
}

一般来说,new 一个对象时小括号后应该是分号,也就是new出对象该语句就结束了。但是出现匿名内部类就不一样,小括号后跟的是大括号,大括号中是该new 出对象的具体的实现方法。因为我们知道,一个抽象类是不能直接new 的,必须先有实现类了我们才能new出它的实现类。上面的伪代码就是表示new 的是Father的实现类,这个实现类是个匿名内部类。
 其实拆分上面的匿名内部类可为:

class SonOne extends Father{
  ...       //这里的代码和上面匿名内部类,大括号中的代码是一样的
}
public class Test{
   Father f1 = new SonOne() ;
}

运行结果:eat something
可以看到,我们直接将抽象类Person中的方法在大括号中实现了,这样便可以省略一个类的书写。并且,匿名内部类还能用于接口上。

public class JavaTest2 {
	public static void main(String[] args) {
		Person per = new Person() {
			public void say() {// 匿名内部类自定义的方法say
				System.out.println("say方法调用");
			}
			@Override
			public void speak() {// 实现接口的的方法speak
				System.out.println("speak方法调用");
			}
		};
		per.speak();// 可调用
		per.say();// 出错,不能调用
	}
}
 
interface Person {
	public void speak();
}

这里per.speak()是可以正常调用的,但per.say()不能调用,为什么呢?注意Person per = new Person()创建的是Person的对象,而非匿名内部类的对象。其实匿名内部类连名字都没有,你咋实例对象去调用它的方法呢?但继承父类的方法和实现的方法是可以正常调用的,本例子中,匿名内部类实现了接口Person的speak方法,因此可以借助Person的对象去调用。
若你确实想调用匿名内部类的自定义的方法say(),当然也有方法:
 (1)类似于speak方法的使用,先在Person接口中声明say()方法,再在匿名内部类中覆写此方法。
 (2)其实匿名内部类中隐含一个匿名对象,通过该方法可以直接调用say()和speak()方法;代码修改如下:

public class JavaTest2 {
	public static void main(String[] args) {
		new Person() {
			public void say() {// 匿名内部类自定义的方法say
				System.out.println("say方法调用");
			}
 
			@Override
			public void speak() {// 实现接口的的方法speak
				System.out.println("speak方法调用");
			}
		}.say();// 直接调用匿名内部类的方法
	}
}
interface Person {
	public void speak();
}

 

lambda表达式

Java8被称作Java史上变化最大的一个版本。其中包含很多重要的新特性,最核心的就是增加了Lambda表达式和Stream API。
是一种用于表示匿名函数和闭包的运算符

lambda表达式本质上是一个匿名方法

lambda表达式主要用于替换以前广泛使用的内部匿名类,各种回调,比如事件响应器、传入Thread类的Runnable等。

Swing编程中的代码,给Button绑定一个监听事件,当点击Button时会在控制台输出"Button Pressed!"内容。这里使用了创建了一个匿名内部类的实例来绑定到监听器,这也是以往比较常规的代码组织形式。但是仔细看一下我们会发现,实际上我们真正关注的就是一个ActionEvent类型的参数e和向控制台输出的语句System.out.println("Button Pressed!");。

public class SwingTest {
    public static void main(String[] args) {
        JFrame jFrame = new JFrame("My JFrame");
        JButton jButton = new JButton("My JButton");

        jButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {                
                System.out.println("Button Pressed!");
            } 
        }); 
        
        jFrame.add(jButton); jFrame.pack(); 
        jFrame.setVisible(true); 
        jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
    }
}

如果将上段程序中以匿名内部类的方式创建接口实例的代码替换成Lambda表达式后

public static void main(String[] args) {
    JFrame jFrame = new JFrame("My JFrame");
    JButton jButton = new JButton("My JButton");

    jButton.addActionListener(e -> System.out.println("Button Pressed!"));

    jFrame.add(jButton);
    jFrame.pack();
    jFrame.setVisible(true);
    jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}

这里参数的类型程序可以根据上下文进行推断,但是并不是所有的类型都可以推断出来,此时就需要我们显示的声明参数类型,当只有一个参数时小括号可以省略。

在Java中,我们是否无法将函数作为参数传递给一个方法,也无法声明返回值是一个函数的方法。在Java8之前,答案是肯定的。
那么,在上面的例子中我们居然可以将一段代码逻辑作为参数传递给了监听器,告诉监听器事件触发时你可以这么做,而不再需要以匿名内部类的方式作为参数。这也是Java8带来的另一新特性:函数式编程。

Lambda表达式的类型是函数。而在Java中,Lambda表达式是对象,它们必须依附于一类特别的对象类型——函数式接口(Functional Interface)。

 

Lambda表达式的语法
基本语法:
(parameters) -> expression

(parameters) ->{ statements; }

public int add(int x, int y) {
  return x + y;
}

转成lambda表达式后是这个样子:
    
(int x, int y) -> x + y;

参数类型也可以省略,Java编译器会根据上下文推断出来:

(x, y) -> x + y; //返回两数之和
 
或者

(x, y) -> { return x + y; } //显式指明返回值

下面这个例子里的lambda表达式没有参数,也没有返回值(相当于一个方法接受0个参数,返回void,其实就是Runnable里run方法的一个实现):

    () -> { System.out.println("Hello Lambda!"); }

如果只有一个参数且可以被Java推断出类型,那么参数列表的括号也可以省略:

  c -> { return c.size(); }

 Lambda表达式的一个重要用法是简化某些匿名内部类(Anonymous Classes)的写法
 实际上Lambda表达式并不仅仅是匿名内部类的语法糖,JVM内部是通过invokedynamic指令来实现Lambda表达式的。
 Lambda表达式并不能取代所有的匿名内部类,只能用来取代函数接口(Functional Interface)的简写
 如果需要新建一个线程,一种常见的写法是这样:

用匿名内部类的方式来创建一个线程

JDK7 匿名内部类写法

new Thread(new Runnable(){// 接口名
    @Override
    public void run(){// 方法名
        System.out.println("Thread run()");
    }
}).start();

上述代码给Tread类传递了一个匿名的Runnable对象,重载Runnable接口的run()方法来实现相应逻辑。这是JDK7以及之前的常见写法。匿名内部类省去了为类起名字的烦恼,但还是不够简化。

函数式接口实例的创建可以有三种方式(参考自FunctionalInterface注解说明):
1.Lambda表达式
2.方法引用
3.构造方法引用

创建函数式接口类型的实例其中一种方式是使用Lambda表达式

JDK8 Lambda表达式写法

Runnable接口的声明,在Java8后,Runnable接口多了一个FunctionalInterface注解,表示该接口是一个函数式接口。但是如果我们不添加FunctionalInterface注解的话,如果接口中有且只有一个抽象方法时,编译器也会把该接口当做函数式接口看待。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
new Thread(
        () -> System.out.println("Thread run()")// 省略接口名和方法名
).start();

上述代码跟匿名内部类的作用是一样的,但比匿名内部类更进一步。

这里连接口名和函数名都一同省掉了,写起来更加神清气爽。如果函数体有多行,可以用大括号括起来,就像这样:

new Thread(
        () -> {
            System.out.print("Hello");
            System.out.println(" Hoolee");
        }
).start();
// 使用匿名内部类  
Runnable race1 = new Runnable() {  
    @Override  
    public void run() {  
        System.out.println("Hello world !");  
    }  
};  
  
// 使用 lambda expression  
Runnable race2 = () -> System.out.println("Hello world !");  
   
// 直接调用 run 方法(没开新线程哦!)  
race1.run();  
race2.run(); 

 

Lambda表达式带参函数的简写
如果要给一个字符串列表通过自定义比较器,按照字符串长度进行排序,Java 7的书写形式如下:

 JDK7 匿名内部类写法

List<String> list = Arrays.asList("I", "love", "you", "too");
Collections.sort(list, new Comparator<String>(){// 接口名
    @Override
    public int compare(String s1, String s2){// 方法名
        if(s1 == null)
            return -1;
        if(s2 == null)
            return 1;
        return s1.length()-s2.length();
    }
});

上述代码通过内部类重载了Comparator接口的compare()方法,实现比较逻辑。采用Lambda表达式可简写如下:

 JDK8 Lambda表达式写法

List<String> list = Arrays.asList("I", "love", "you", "too");
Collections.sort(list, (s1, s2) ->{// 省略参数表的类型
    if(s1 == null)
        return -1;
    if(s2 == null)
        return 1;
    return s1.length()-s2.length();
});

上述代码跟匿名内部类的作用是一样的。除了省略了接口名和方法名,代码中把参数表的类型也省略了。这得益于javac的类型推断机制,编译器能够根据上下文信息推断出参数的类型,当然也有推断失败的时候,这时就需要手动指明参数类型了。注意,Java是强类型语言,每个变量和对象都必需有明确的类型。

能够使用Lambda的依据是必须有相应的函数接口(函数接口,是指内部只有一个抽象方法的接口)。这一点跟Java是强类型语言吻合,也就是说你并不能在代码的任何地方任性的写Lambda表达式。实际上Lambda的类型就是对应函数接口的类型。Lambda表达式另一个依据是类型推断机制,在上下文信息足够的情况下,编译器可以推断出参数表的类型,而不需要显式指名。Lambda表达更多合法的书写形式如下:

Lambda表达式的书写形式

Runnable run = () -> System.out.println("Hello World");// 1
ActionListener listener = event -> System.out.println("button clicked");// 2
Runnable multiLine = () -> {// 3 代码块
    System.out.print("Hello");
    System.out.println(" Hoolee");
};
BinaryOperator<Long> add = (Long x, Long y) -> x + y;// 4
BinaryOperator<Long> addImplicit = (x, y) -> x + y;// 5 类型推断

1展示了无参函数的简写;2处展示了有参函数的简写,以及类型推断机制;3是代码块的写法;4和5再次展示了类型推断机制。

 

集合类(包括List)现在都有一个forEach方法,对元素进行迭代(遍历),所以我们不需要再写for循环了。forEach方法接受一个函数接口Consumer做参数,所以可以使用λ表达式。

Java8之前集合类的迭代(Iteration)都是外部的,即客户代码。而内部迭代意味着改由Java类库来进行迭代,而不是客户代码。

    for(Object o: list) { // 外部迭代
        System.out.println(o);
    }

forEach方法,它就是Java8中新增加的默认方法。

public interface Iterable<T> {

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
}

可以写成:

   list.forEach(o -> {System.out.println(o);}); //forEach函数实现内部迭代

Iterable接口新增了一个forEach(Consumer action)默认方法,该方法所需参数的类型是一个函数式接口,而Iterable接口是Collection接口的父接口,因此Collection集合也可以直接调用该方法。

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
list.forEach(new Consumer<Integer>() {
@Override
 public void accept(Integer integer) {
       System.out.println(integer);
    }
});
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
//Lambda表达式 接收一个参数 不返回值
list.forEach(item -> System.out.println(item));
}

该lambda表达式item -> System.out.println(item)接收一个参数 不返回值,符合accept方法定义,编译通过。
也就是说如果使用lambda表达式来创建一个函数式接口实例,那这个lambda表达式的入参和返回必须符合这个函数式接口中唯一的抽象方法的定义。

当程序调用Iterable的forEach(Consumer action)遍历集合元素时,程序会依次将集合元素传给Consumer的accept(T t)方法(该接口中唯一的抽象方法)。正因为Consumer是函数式接口,因此可以使用Lambda表达式来遍历集合元素。

//创建一个集合
Collection<String> c=new HashSet<String>();
//需要泛型,否则提示警告:使用了未经检查或不安全的操作,可以直接运行
c.add("ni");
c.add("hao");
c.add("java");
c.forEach(new Consumer<String>(){
   public void accept(String t){
    System.out.println("集合元素是:"+t);
   }
});
//不使用lambda表达式和两种使用lambda表达式方式
c.forEach(t->System.out.println("集合元素是:"+t));
c.forEach(System.out::println);

Iterator新增了一个forEachRemaining(Consumer action)方法,该方法所需的Consumer参数同样也是函数式接口。当程序调用Iterator的forEachRemaining(Consumer action)遍历集合元素时,程序会一次将集合元素传递给Consumer的accept(T t)方法(该接口中唯一的抽象方法)。

Collection<String> c=new HashSet<String>();
c.add("ni");
c.add("hao");
c.add("java");
Iterator<String> it=c.iterator();//使用泛型
it.forEachRemaining(System.out::println);

遍历数组

String[] atp = {"Rafael Nadal", "Novak Djokovic",  
       "Stanislas Wawrinka",  
       "David Ferrer","Roger Federer",  
       "Andy Murray","Tomas Berdych",  
       "Juan Martin Del Potro"};  
List<String> players =  Arrays.asList(atp);  
  
// 以前的循环方式  
for (String player : players) {  
     System.out.print(player + "; ");  
}  
  
// 使用 lambda 表达式以及函数操作(functional operation)  
players.forEach((player) -> System.out.print(player + "; "));  
   
// 在 Java 8 中使用双冒号操作符(double colon operator)  
players.forEach(System.out::println);  

 

lambda表达式可以访问给它传递的变量,访问自己内部定义的变量,同时也能访问它外部的变量。
不过lambda表达式访问外部变量有一个非常重要的限制:变量不可变(只是引用不可变,而不是真正的不可变)。
当在表达式内部修改waibu = waibu + " ";时,IDE就会提示你:
Local variable waibu defined in an enclosing scope must be final or effectively final
编译时会报错。因为变量waibu被lambda表达式引用,所以编译器会隐式的把其当成final来处理。
以前Java的匿名内部类在访问外部变量的时候,外部变量必须用final修饰。现在java8对这个限制做了优化,可以不用显示使用final修饰,但是编译器隐式当成final来处理

变量waibu :外部变量
变量chuandi :传递变量
变量zidingyi :内部自定义变量

将为列表中的字符串添加前缀字符串

String waibu = "lambda :";
List<String> proStrs = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
List<String>execStrs = proStrs.stream().map(chuandi -> {
Long zidingyi = System.currentTimeMillis();
return waibu + chuandi + " -----:" + zidingyi;
}).collect(Collectors.toList());
execStrs.forEach(System.out::println);

输出:

lambda :Ni -----:1474622341604
lambda :Hao -----:1474622341604
lambda :Lambda -----:1474622341604

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wespten

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值