【lambda】java8 lambda

今天面试的时候,贵公司竟然已经都在全部使用java 8 了,学习一下,省得成为门槛!

详情参看 oralce 官网的 : Java SE 8:Lambda Quick Start

1. 介绍

Lambda 表达式是 Java 8 的新特性,通过使用一个表达式来代表一个方法接口。同时Lambda 表达式 使 从 集合中迭代数据 和 取数据 更简单。

 

2. 背景

匿名内部类(Anonymous Inner Class)

在 Java 中,匿名内部类 提供了 一种类的实现方式,这种类的实现方法很方便,随时需要随时都可以进行不同的实现,而不需要单独再去写一个实现类。比如 Swing 或者 JavaFX 中的事件监听机制代码:

 

        JButton button = new JButton("Test Button");
        button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("Click detected by anon class");
            }
        });

这种实现方法方便阅读,但是 代码不够优雅:因为 为了定义一个方法需要太多的代码;

 

 

函数式接口(Functional Interfaces)

上面提到的 ActionListener 接口 在 Jdk 8 中是如下定义的:

 

package java.awt.event;

import java.util.EventListener;

/**
 * @author Carl Quinn
 * @since 1.1
 */
public interface ActionListener extends EventListener {

    /**
     * Invoked when an action occurs.
     */
    public void actionPerformed(ActionEvent e);

}


这个接口的特点 : 接口仅仅包含一个 方法,遵循这种模式的接口 在 Java 8 中 被称为 函数式接口(functional interface)

 

Note:这种类型的接口 在之前被 称为 单一抽象方法类型 SAM(Single Abstract Method);

在 Java 中 通过 匿名内部类 来使用函数式接口 是一个 很常见的模式,除了 EventListener 类,像 Runnable ,Comparator 接口 也是以相似的方式被使用;因此 通过 lambda 表达式的使用 来促使 函数式接口的改变。

 

Lambda 表达式语法

Lambda 表达式 通过 将 五行代码 转变为 一条单独的声明 来 解决 匿名内部类 臃肿的内码;

Lambda 表达式 由三部分组成:

      ★  参数列表  : (int x,int y)

      ★  箭头标记   :  ->

      ★  body 体     :  x + y

body 可以是 单个表达式,也可以是一个语句块;在表达式中,body 已经经过简单的计算并且返回;在语句块中,body 像 一个方法体一样被计算 并且 返回语句 返回 对 匿名方法 的 调用者的控制;在顶层使用 break 和 continue 关键字 是非法的 ,但是 在 循环中是允许的 ;如果 body  返回一个结果,每个控制路径必须 返回 或者 抛出一个异常;

一些简单的栗子:

 

(int x, int y) -> x + y

() -> 42

(String s) -> { System.out.println(s); }


第一个栗子: 有两个Integer 类型的参数,x 和 y;使用表达式的形式 返回  x+y;

第二个栗子:无参数;使用表达式的形式返回 一个 Integer 值 42;

第三个栗子:有一个字符串参数,s;使用语句块的形式将字符串打印到控制台,并且无返回值;

 

3.Lambda 栗子

Runnable Lambda:使用语句块的形式,将五行代码转变为一条语句;

 

package com.ycit.lambda;

/**
 * Created by xlch on 2017/3/2.
 */
public class RunnableTest {

    public static void main(String [] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("normal Anonymous Inner class");
            }
        };

        Runnable runnable1 = () -> System.out.println("Lambda Expression");

        runnable.run(); //输出 normal Anonymous Inner class
        runnable1.run();//输出 Lambda Expression
    }
}


Comparator 栗子

根据 Person 的 name 排序:

 

package com.ycit.bean;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by xlch on 2017/3/3.
 */
public class Person {

    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //getter setter ignore

    public static List<Person> createList() {
        Person p1 = new Person("bob",21);
        Person p2 = new Person("john",25);
        Person p3 = new Person("jack",23);
        Person p4 = new Person("amy",27);
//        List<Person> persons = ImmutableList.of(p1, p2, p3, p4);不可变的list
        List<Person> persons = new ArrayList<>();
        persons.add(p1);
        persons.add(p2);
        persons.add(p3);
        persons.add(p4);
        return persons;
    }
}


ComparatorTest

 

 

package com.ycit.lambda;

import com.ycit.bean.Person;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * Created by xlch on 2017/3/3.
 */
public class ComparatorTest {

    public static void main(String[] args) {
        List<Person> persons = Person.createList();
        Collections.sort(persons, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getName().compareTo(o2.getName());
            }
        });
        System.out.println("===normal sorted asc name===");
        for (Person person:persons) {
            System.out.println(person.getName());
        }
        System.out.println("=====lambda sorted desc name=======");
        Collections.sort(persons,(Person p1, Person p2) -> p2.getName().compareTo(p1.getName()));
        for (Person person:persons) {
            System.out.println(person.getName());
        }
        System.out.println("=====lambda sorted asc name=======");
        Collections.sort(persons, (p1, p2) -> p1.getName().compareTo(p2.getName()));
        for (Person person:persons) {
            System.out.println(person.getName());
        }
    }

}


第二种 Lambda 表达式 省略了 参数的 类型声明,Lambda 表达式 支持 目标类型(target typing),目标类型是 根据 使用的上下文来推断对象类型;因为我们是把结果分配给了 用泛型定义的 Comparator. 编译器可以推断出这两个参数类型为 Person;

ActionListener 栗子:

package com.ycit.lambda;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
 * Created by xlch on 2017/3/1.
 */
public class InnerClass {

    public static void main(String[] args) {
        JButton button = new JButton("Test Button");
        button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("Click detected by anon class");
            }
        });

        button.addActionListener(e -> System.out.println("Click detected by lambda expression"));

        // Swing stuff
        JFrame frame = new JFrame("Listener Test");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(button, BorderLayout.CENTER);
        frame.pack();
        frame.setVisible(true);
    }

}


Lambda 表达式 是作为参数被传递, Target Typing 被 用于以下场景:

 

   ★ 变量声明

   ★ assignment

   ★返回语句

   ★数组初始化

   ★ 方法或者构造函数参数

   ★ Lambda 表达式的 body

   ★ 条件表达式

   ★ 强制转换表达式

  

最佳实践建议:

尽量保证 lambda 表达式的精简:

 

  • 避免 lambda 体 中过多的代码块,可以通过调用方法来实现;
   @Test
    public void practise() {
        Function<String, String> function = this::buildStr;
//        Function<String, String> function1 = param -> buildStr(param); // 和此等价
        String result = add(" Message ", function);
        System.out.println(result);

    }

    public String buildStr(String param) {
        //many code
        return " something " + param;
    }

    public String add(String str, Function<String, String> function) {
        return function.apply(str);
    }

 

  • 避免指定 参数类型 ,可以直接省略:

像这样:

(a, b) -> a.toLowerCase() + b.toLowerCase();

而不是这样:

(String a, String b) -> a.toLowerCase() + b.toLowerCase();

 

 

  • 避免返回语句 和 括号:

像这样:

	
a -> a.toLowerCase();

而不是这样:

a -> {return a.toLowerCase();};

避免单个参数时使用括号:

像这样:

a -> a.toLowerCase();

而不是这样:

(a) -> a.toLowerCase();

 

  • 使用方法引用

lambda 表达式:

a -> a.toLowerCase();

方法引用:

String::toLowerCase;

 

4.使用 Lambda 表达式 改进代码

 

Lambda 表达式 应该为 DRY(Don`t Repeat Yourself)原则提供更好的支持,并且使代码 更简单,更可读;

一个常见的查询案例

从一个人员列表中找出:

           适合做司机的人(年龄大于16岁)

           可以应征入伍的人(年龄大于18岁小于25岁的男性)

           有资格做飞行员的人(年龄在23-65 之间的人)

然后通过不同的方式通知这些人(打电话,发邮件,传真等等);

初次尝试

你可能会写出如下类似的代码方法供外部使用(或者多个业务集中在一个方法中):

 

package com.ycit.lambda;

/**
 * Created by xlch on 2017/3/4.
 */

import com.ycit.bean.Gender;
import com.ycit.bean.Person;

import java.util.List;

/**
 *
 * Drivers: Persons over the age of 16
 * Draftees: Male persons between the ages of 18 and 25
 * Pilots (specifically commercial pilots): Persons between the ages of 23 and 65
 */
public class QueryCase {

    public void callDriver(List<Person> persons) {
        for (Person person:persons) {
            if (person.getAge() >= 16){
                call(person);
            }
        }
    }

    public void emailDraftees(List<Person> persons) {
        for (Person p:persons) {
            if (p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE){
                email(p);
            }
        }
    }

    public void mailPilots(List<Person> persons) {
        for (Person p:persons) {
            if (p.getAge() >= 23 && p.getAge() <= 65){
                mail(p);
            }
        }
    }

    /**-----------通讯方式----------------**/

    public void call(Person person) {
        System.out.println("call :" +person.getName());
    }

    public void mail(Person person) {
        System.out.println("mail:" + person.getName());
    }

    public void email(Person person) {
        System.out.println("email:" + person.getName());
    }

}


从代码的方法名定义中(callDriver,emailDraftees,mailPilots)我们可以看出,这个方法描述了正在发生的行为的类别 ;查询条件被明确地和 动作调用绑定到一起;导致了代码不够灵活


以上设计的代码有以下不好的地方:

 

 

    ★ 违背了  DRY 原则 : 每个方法重复的循环 ;每个方法中 选择方法都需要重写;

    ★ 每个使用案例都需要实现大量的方法;

    ★ 代码不灵活。如果更改了搜索条件,则需要更新一些代码。因此,代码是不可维护的。

方法重构

从查询条件 重构开始 :如果 判断条件 被分离到单独的方法中,那么这就算是一个改进:

 

package com.ycit.lambda;

/**
 * Created by xlch on 2017/3/4.
 */

import com.ycit.bean.Gender;
import com.ycit.bean.Person;

import java.util.List;

/**
 *
 * Drivers: Persons over the age of 16
 * Draftees: Male persons between the ages of 18 and 25
 * Pilots (specifically commercial pilots): Persons between the ages of 23 and 65
 */
public class QueryCase {

    public void callDriver(List<Person> persons) {
        for (Person person:persons) {
            if (isDriver(person)){
                call(person);
            }
        }
    }

    public void emailDraftees(List<Person> persons) {
        for (Person p:persons) {
            if (isDraftee(p)){
                email(p);
            }
        }
    }

    public void mailPilots(List<Person> persons) {
        for (Person p:persons) {
            if (isPilot(p)){
                mail(p);
            }
        }
    }

    /**-----------条件判断----------------**/

    public boolean isDriver(Person p) {
        return p.getAge() >= 16;
    }

    public boolean isDraftee(Person p) {
        return p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
    }

    public boolean isPilot(Person p) {
        return p.getAge() >= 23 && p.getAge() <= 65;
    }

    /**-----------通讯方式----------------**/

    public void call(Person person) {
        System.out.println("call :" +person.getName());
    }

    public void mail(Person person) {
        System.out.println("mail:" + person.getName());
    }

    public void email(Person person) {
        System.out.println("email:" + person.getName());
    }

}

 

 

 

查询条件被封装在单独的方法中,相对之前的代码有所提升。查询条件可以重复利用,并且可以在整个类中反复变化。
但是 对于每个使用案例,仍然需要大量的重复代码 和 一个单独的方法。那么有没有更好的方法传递查询条件给方法呢?

匿名类

在 Lambda 表达式之前,使用匿名内部类是一种选择。

例如,一个接口中包含一个 返回 boolean 的 test 方法(函数式接口),查询条件可以在方法被调用的时候传递,像这样:

 

package com.ycit.lambda;

/**
 * Created by xlch on 2017/3/5.
 */
public interface MyTest<T> {

    boolean test(T t);

}


使用该接口

 

 

package com.ycit.lambda;

import com.ycit.bean.Person;

import java.util.List;

/**
 * Created by xlch on 2017/3/5.
 */
public class QueryCaseWithInnerClass {

    public void callContacts(List<Person> persons, MyTest<Person> myTest) {
        for (Person person:persons) {
            if (myTest.test(person)){
                call(person);
            }
        }
    }

    public void emailContacts(List<Person> persons, MyTest<Person> myTest) {
        for (Person p:persons) {
            if (myTest.test(p)){
                email(p);
            }
        }
    }

    public void mailContacts(List<Person> persons, MyTest<Person> myTest) {
        for (Person p:persons) {
            if (myTest.test(p)){
                mail(p);
            }
        }
    }

    /**-----------通讯方式----------------**/

    public void call(Person person) {
        System.out.println("call :" +person.getName());
    }

    public void mail(Person person) {
        System.out.println("mail:" + person.getName());
    }

    public void email(Person person) {
        System.out.println("email:" + person.getName());
    }

}


看上去很好,但是当以上方法被调用时,代码就有点丑陋了:

 

 

package com.ycit.lambda;

import com.ycit.bean.Gender;
import com.ycit.bean.Person;

import java.util.List;

/**
 * Created by xlch on 2017/3/5.
 */
public class UseInnerClassTet {

    public static void main(String [] args) {

        List<Person> persons = Person.createList();
        QueryCaseWithInnerClass innerClass = new QueryCaseWithInnerClass();

        innerClass.callContacts(persons, new MyTest<Person>() {
            @Override
            public boolean test(Person person) {
                return person.getAge() >=16;
            }
        });

        innerClass.emailContacts(persons, new MyTest<Person>() {
            @Override
            public boolean test(Person p) {
                return  p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
            }
        });

        innerClass.mailContacts(persons, new MyTest<Person>() {
            @Override
            public boolean test(Person p) {
                return p.getAge() >= 23 && p.getAge() <= 65;
            }
        });
    }

}


这是一个练习 "垂直问题"(vertical problem)的一个很好的栗子;代码有点难阅读,而且我们必须要为每一个使用案例写自定义的查询条件

 

 

 

使用 Lambda 表达式刚刚好

在之前的栗子中, 通过 MyTest 函数式接口将 匿名类传递给方法。然而,在 Java SE8 中不需要写那样的接口,它提供了含有大量标准 函数式接口的  java.util.function 包。

在这个案例中,Predicate 接口符合我们的需求。

其中部分源码:

 

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
    //....
}


修改后

package com.ycit.lambda;

import com.ycit.bean.Person;

import java.util.List;
import java.util.function.Predicate;

/**
 * Created by xlch on 2017/3/5.
 */
public class QueryCaseWithFunction {

    public void callContacts(List<Person> persons, Predicate<Person> predicate) {
        for (Person person:persons) {
            if (predicate.test(person)){
                call(person);
            }
        }
    }

    public void emailContacts(List<Person> persons, Predicate<Person> predicate) {
        for (Person p:persons) {
            if (predicate.test(p)){
                email(p);
            }
        }
    }

    public void mailContacts(List<Person> persons, Predicate<Person> predicate) {
        for (Person p:persons) {
            if (predicate.test(p)){
                mail(p);
            }
        }
    }

    /**-----------通讯方式----------------**/

    public void call(Person person) {
        System.out.println("call :" +person.getName());
    }

    public void mail(Person person) {
        System.out.println("mail:" + person.getName());
    }

    public void email(Person person) {
        System.out.println("email:" + person.getName());
    }

}


垂直问题的解决

 

Lambda 表达式解决了垂直问题, 让 Lambda 表达式得到了简单的重用,

 

package com.ycit.lambda;

import com.ycit.bean.Gender;
import com.ycit.bean.Person;

import java.util.List;
import java.util.function.Predicate;

/**
 * Created by xlch on 2017/3/5.
 */
public class UseLambdaTest {

    public static void main(String[]args) {
        List<Person> persons = Person.createList();
        QueryCaseWithFunction robo = new QueryCaseWithFunction();

        Predicate<Person> drivers = person -> person.getAge() >= 16;
        Predicate<Person> draftees = person -> person.getAge() >= 18 && person.getAge() <= 25 && person.getGender() == Gender.MALE;
        Predicate<Person> pilots = person -> person.getAge() >= 23 && person.getAge() <= 65;

        robo.callContacts(persons, drivers);
        robo.callContacts(persons, draftees);
        robo.callContacts(persons, pilots);
        robo.emailContacts(persons, draftees);
        robo.mailContacts(persons, pilots);
    }

}


为每一个分组都创建了一个 Predicate,你可以传递任意一个分组到 联系方式的方法中。


5.java.util.function 包

 

Java SE8 中提供了大量函数式接口:

       ★  Predicate :对象作为参数传递的一个属性;代表一个参数的 断言(predicate) (相当于返回 boolean 值的 Function 接口);

 

public interface Predicate<T> {
    boolean test(T t);
}

 

       ★  Consumer:对象作为参数传递的将要被执行的一个动作;代表这样一种操作:接收一个单独的输入参数,不返回任何结果

public interface Consumer<T> {
    void accept(T t);
}

 

       ★  Function: 把 T 转为 U ;代表这样一种函数:接收一个参数,返回另一个参数

 

public interface Function<T, R> {
    R apply(T t);
}

 

       ★  Supplier: 提供 T 的一个实例(例如一个工厂):代表 结果的 提供者

 

public interface Supplier<T> {
    T get();
}

 

       ★  UnaryOperator: 从 T 到 T 的一个一元操作

 

public interface UnaryOperator<T> extends Function<T, T> {
    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}

 

       ★  BinaryOperator:从(T,T)到 T 的 一个二元操作;

 

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
    public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
    }

    public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
    }
}

 

另外,这些接口中的大多数 都有 原型版本,这将是一个很好的开始

东方风格的名字 和 相关方法

由于东方和西方的文化差异,在名字的写法上有差别,

东方人是把 姓写在前面,名写在后面;西方人是把名写在前面,姓写在后面;

没有使用 Lambda 表达式 的老方法

Person.java

 

    public void printWesternName() {
        System.out.println("western name:" + this.getGivenName() + " " + this.getSurName());
    }

    public void printEasternName() {
        System.out.println("eastern name:" + this.getSurName() + " " + this.getGivenName());
    }

 

Function 函数式接口

Function 接口可以解决这个问题,仅仅有一个 抽象方法:将 T 转为 R

 

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    //....
}

 

使用 Lambda 表达式解决:

 

package com.ycit.lambda;

import com.ycit.bean.Person;

import java.util.List;
import java.util.function.Function;

/**
 * Created by xlch on 2017/3/5.
 */
public class FunctionTest {

    public static void main(String [] args) {
        // ① 传统的方法
        System.out.println("==========传统解决方法");
        List<Person> persons = Person.createList();
        for (Person person:persons) {
            person.printEasternName();
            person.printWesternName();
        }

        //② 自定义
        System.out.println("=========自定义");
        for (Person person:persons) {
            System.out.println(
                    person.printCustom(p -> "name:" + p.getGivenName())
            );
        }
        //③ 提前定义 Lambda 表达式
        Function<Person,String> westernNames = p -> "western name:" + p.getGivenName() + " " + p.getSurName();

        Function<Person,String> easternNames = p -> "eastern name:" + p.getSurName() + " " + p.getGivenName();

        System.out.println("============print western name");
        for (Person person:persons) {
            System.out.println(
                    person.printCustom(westernNames)
            );
        }

        System.out.println("============print eastern name");
        for (Person person:persons) {
            System.out.println(
                    person.printCustom(easternNames)
            );
        }

    }

}


6.Lambda 表达式 和 集合

 

循环(Looping)

第一个新特色是 对任何的 集合类 都可以使用的新方法 : forEach

栗子:

 

        List<Person> persons = Person.createList();
        persons.forEach(p -> p.printEasternName());
        persons.forEach(Person :: printEasternName);
        persons.forEach(p -> System.out.println(p.printCustom(e -> "giveName is " + e.getGivenName())));

 

第一个 forEach 是 标准化 的Lambda 表达式的 使用;

第二个 展示了 一种方法引用(method reference),对类中已经存在的方法执行操作,这个语法可以用来代替 正常的 Lambda 表达式语法;

forEach 方法的顶层为  java.lang.Iterable 接口中的方法,源码:

 

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

 

底层使用了 for 循环对集合的每一个元素进行处理;

参数为 Consumer 接口(函数式接口),该接口 代表了 一类操作 :接收一个参数,无返回值;
 

链 和 过滤 (Chaining and Filters)

对集合 先过滤再 循环:

 

        System.out.println("=====filter");
        List<Person> persons = Person.createList();
        persons.stream().filter(p -> p.getAge() >= 30).forEach(p -> System.out.println(p.getAge()));


惰性

对于已经有一个 极好的 for 循环 ,为什么还给集合增加这些方法呢?

通过将迭代特征移动到类库中,这允许 Java 开发者 做更多的代码优化。为了进一步的解释,需要定义两个术语:

    ★ Laziness:在编程中,惰性是指 当你需要去处理对象时,只处理你想要处理的对象,对于上面的 先过滤再循环就是一种 lazy

    ★ Eagerness:对 列表中的每一个对象都执行操作的代码被称为 " eager",例如 加强版的 for 循环;

 

stream 方法

stream 简介图:

Stream 定义:

  • 支持 串行  和 并行 聚合操作的 元素序列。

java.util.Collection  中的 stream 方法源码:

 

    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }

 

一个 Stream  代表 可以链接各种方法的 有序元素。默认情况下, 元素一旦被消费掉,那么将不再可用;因此,一连串的操作在指定的 Stream 上只能出现一次;而且,一个 Stream 的 串行(serial,默认)和并行(parallel)取决于被调用的方法;

除了对象引用的  Stream , 还提供了 基本数据类型的 Stream: IntStream,LongStream,DoubleStream.

修改 和 结果

一个 Stream 在使用过后会被处理掉。因此,集合中的元素不可以通过 Steam 改变;

但是你可以在一连串的操作之后将结果返回,保存为另一个新的集合。

 

        System.out.println("=====Make a new list after filtering.");
        List<Person> oldAges = persons.stream().filter(p -> p.getAge() >= 80).collect(Collectors.toList());
        oldAges.forEach(p -> System.out.println(p.printCustom(e -> "age is " + e.getAge())));


Stream 的 collect 方法  传入 Collectors 类 ,Collectors 类 能够根据 Stream 的结果 返回 List 或者 Set

 

使用 map 计算

        System.out.println("===计算total average");
        long totalAge = persons.stream().filter(p -> p.getAge() >= 30).mapToInt(p -> p.getAge()).sum();
        System.out.println("totalAge ==" + totalAge);
        OptionalDouble average = persons.stream().filter(p -> p.getAge() >= 30).mapToDouble(p -> p.getAge()).average();
        System.out.println("average age ==" + average.getAsDouble());

 

计算 总和:使用 map 方法 以 串行的方式获取 每个人的年龄 ,最后求和;


 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值