文章目录
多线程
线程概述
在了解线程之前,要先理解程序和进程的概念。
程序:程序是一组指令的集合,是静态的,比如我们安装的软件,在没有运行时的状态。
进程:进程是程序动态执行的过程,如运行中的软件。程序是静态的,而进程是动态的。进程也是资源分配的基本单位,系统在运行时会为每个进程分配不同的内存区域。
线程:进程又可进一步细化为线程,一个进程至少有一个主线程。线程作为调度和执行的基本单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小。一个进程中的多个线程共享相同的内存单元/内存地址空间,他们从同一个堆中分配对象,可以访问相同的变量和对象,这就使得线程间通信更简便、高效,但是多个线程操作共享的系统资源可能会带来安全隐患(线程安全和线程不安全)。
Thread类实现多线程
Thread类在java.lang包下,通过继承该类并重写它的run()方法,然后调用start()方法即可开启一个线程。
public class MyThread extends Thread{
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("线程2*****"+i+"*****");
}
}
/**
* 测试
* @param args
*/
public static void main(String[] args) {
MyThread thread2 = new MyThread();
thread2.start();//开启线程
for (int i=0;i<100;i++){
System.out.println("主线程-----"+i+"-----");
}
}
}
线程之间的运行是独立的,并不是执行完一个线程再去执行另一个线程。
注:main()方法是主线程。
Thread类中的一些常用方法
方法名 | 说明 |
---|---|
void start() | 启动线程,并执行对象的run()方法 |
void run() | 线程在被调度时会执行该方法 |
String getName() | 返回线程的名称 |
void setName(String name) | 设置该线程的名称 |
static Thread currentThread() | 返回当前线程 |
static void sleep(long millis) | 使当前线程在指定时间段内放弃对CPU的控制,时间到后重新排队 |
static void yield() | 线程让步(释放CPU,系统重新调度) |
join() | 当线程A调用线程B的join()方法时,线程A将被阻塞,转而执行B线程,直至B线程执行完毕才会执行线程A |
stop() | 强制结束当前线程的生命周期(已过时,不推荐使用) |
boolean isAlive() | 判断当前线程是否还活着 |
public class MyThread extends Thread{
@Override
public void run() {
for (int i=0;i<100;i++){
if (i%10==0)
this.yield();//当执行到10的倍数时,释放CPU
System.out.println(this.getName()+"-----"+i);//获取当前线程名
}
}
/**
* 测试
* @param args
*/
public static void main(String[] args) throws InterruptedException {
MyThread thread2 = new MyThread();
thread2.setName("线程2");//为线程thread2设置线程名
thread2.start();//开启线程
Thread.currentThread().setName("线程1");//为主线程设置线程名
for (int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"-----"+i);
if (i==1)
thread2.join();//当主线程执行到1时,让线程thread2执行完毕,然后主线程才会执行
}
}
}
结果就是在主线程执行到1之前,线程1和线程2是有交互的。当线程1执行到1后,线程1被阻塞,线程2将所有数打印完后,才会执行线程1.
线程的优先级
Thread类定义的一些优先级等级:
值 | 代表的数字 |
---|---|
Thread.MAX_PRIORITY | 10 |
Thread.NORM_PRIORITY | 5 |
Thread.MIN_PRIORITY | 1 |
涉及Thread类的一些方法
方法名 | 说明 |
---|---|
void setPriority(int newPriority) | 设置线程的优先级 |
int getPriority() | 返回线程的优先级代表的数字 |
public static void main(String[] args) throws InterruptedException {
MyThread thread2 = new MyThread();
thread2.setName("线程2");//为线程thread2设置线程名
System.out.println("线程1的优先级:"+Thread.currentThread().getPriority());
System.out.println("线程2的优先级:"+thread2.getPriority());
thread2.start();//开启线程
Thread.currentThread().setName("线程1");//为主线程设置线程名
for (int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
注:线程创建时继承父类的优先级。低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用。
Runnable接口实现多线程
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
创建线程的第二种方式就是实现Runnable接口。主要步骤如下:
- 实现Runnable接口,实现它的run()方法。
- 创建Runnable接口实现类的对象。
- 将实现类对象作为参数传递到Thread类的构造器中,创建Thread类对象
- 调用Thread类对象的start()方法
测试:
(1)创建Runnable接口的实现类
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
(2)创建测试类,开启多个线程
public class TestRunnable {
public static void main(String[] args) {
//创建实现类对象
MyRunnable myRunnable = new MyRunnable();
//创建Thread对象
Thread thread1 = new Thread(myRunnable);
Thread thread2 = new Thread(myRunnable);
Thread thread3 = new Thread(myRunnable);
//为线程设置线程名
thread1.setName("线程1");
thread2.setName("线程2");
thread3.setName("线程3");
//开启线程
thread1.start();
thread2.start();
thread3.start();
}
}
使用Runnable接口的方式创建线程,多个线程使用同一个Runnable实现类的对象,线程之间更容易共享资源,不用加static修饰就可以共享同一个变量。
public class MyRunnable implements Runnable{
private int i = 0;//多个线程可以共享i变量
@Override
public void run() {
for (;i<10;i++){
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
而通过继承Thread创建多线程,需要加static才能共享某一个变量。
public class MyThread extends Thread{
private static int i = 0;//必须加static修饰,多个线程才能共享变量i
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println(this.getName()+"-----"+i);
}
}
/**
* 测试
* @param args
*/
public static void main(String[] args) throws InterruptedException {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
MyThread thread3 = new MyThread();
thread1.setName("线程1");
thread2.setName("线程2");
thread3.setName("线程3");
thread1.start();
thread2.start();
thread3.start();
}
}
Runnable接口和Thread类的关系
通过源码可以看到,Thread类默认实现了Runnable接口。我们往Thread类里传Runnable实现类的对象,就是给变量target赋值了。
线程的生命周期
在jdk1.5之前有5种线程的状态:
- 新建:当一个Thread类或其子类的对象刚被创建时。
- 就绪:调用start()方法后,新建的线程将进入线程队列等待CPU时间片,此时它已经具备了运行的条件,只是没有分配到CPU资源。
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,此时会执行run()方法内的代码。
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
- 死亡:线程已完成全部的工作或被提前强制性终止或出现异常而导致结束。
在jdk1.5之后,线程的状态进行了一些细化,在Thread.State类中定义了线程的几种状态,该类定义在Thread类的内部:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
线程安全
当多个线程同时访问同一资源(变量、文件等)的时候,若多个线程只有读操作,那么是不会发生线程安全问题。但是如果多个线程对资源有读和写的操作,就容易出现线程安全问题。
如下两个代码就可能出现线程安全问题:
public class RATM implements Runnable{
private int balance = 10000;//将要操作的资源
@Override
public void run() {
if (balance>6000){
try {
Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
balance-=6000;
System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//测试
public static void main(String[] args) {
RATM atm = new RATM();
Thread atm1 = new Thread(atm,"atm1");
Thread atm2 = new Thread(atm,"atm2");
atm1.start();
atm2.start();
}
}
public class TATM extends Thread{
private static int balance = 10000;//将要操作的资源
@Override
public void run() {
if (balance>6000){
try {
Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
balance-=6000;
System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//测试
public static void main(String[] args) {
TATM atm1 = new TATM();atm1.setName("atm1");
TATM atm2 = new TATM();atm2.setName("atm2");
atm1.start();
atm2.start();
}
}
它们的结果都是一样的
很明显,出现了线程安全的问题,出现了负值,解决的办法就是加锁,一个线程在操作共享数据时不允许别的线程操作,加锁使用synchronized关键字,有同步代码块和同步方法两种方式。
同步代码块
同步代码块格式
synchronized (同步监视器){
//需要加锁的代码
}
同步监视器可以是任何对象,但必须保证多个线程的同步监视器是一样的。
将上述两个代码加锁,如下:
public class RATM implements Runnable{
private int balance = 10000;//将要操作的资源
@Override
public void run() {
synchronized (this){//加锁
if (balance>6000){
try {
Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
balance-=6000;
System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//测试
public static void main(String[] args) {
RATM atm = new RATM();
Thread atm1 = new Thread(atm,"atm1");
Thread atm2 = new Thread(atm,"atm2");
atm1.start();
atm2.start();
}
}
public class TATM extends Thread{
private static int balance = 10000;//将要操作的资源
@Override
public void run() {//加锁
synchronized (this.getClass()){
if (balance>6000){
try {
Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
balance-=6000;
System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//测试
public static void main(String[] args) {
TATM atm1 = new TATM();atm1.setName("atm1");
TATM atm2 = new TATM();atm2.setName("atm2");
atm1.start();
atm2.start();
}
}
控制台结果如下
同步方法
如果操作共享数据的代码完整的生命在了一个方法中,那么我们可以将此方法声明为同步方法。声明格式如下
权限修饰符 [static] synchronized 返回值类型 方法名(方法参数){
}
注:无法写同步监视器,同步方法有默认的同步监视器,如果是普通方法,同步监视器就是this;如果是静态方法,同步监视器就是该类的class对象。
将上述两个同步代码块抽取出来为一个方法,修改如下:
public class RATM implements Runnable{
private int balance = 10000;//将要操作的资源
@Override
public void run() {
extracted();
}
private synchronized void extracted() {
if (balance>6000){
try {
Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
balance-=6000;
System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//测试
public static void main(String[] args) {
RATM atm = new RATM();
Thread atm1 = new Thread(atm,"atm1");
Thread atm2 = new Thread(atm,"atm2");
atm1.start();
atm2.start();
}
}
public class TATM extends Thread{
private static int balance = 10000;//将要操作的资源
@Override
public void run() {//加锁
extracted();
}
private static synchronized void extracted() {
if (balance>6000){
try {
Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
balance-=6000;
System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//测试
public static void main(String[] args) {
TATM atm1 = new TATM();atm1.setName("atm1");
TATM atm2 = new TATM();atm2.setName("atm2");
atm1.start();
atm2.start();
}
}
测试效果和同步代码块一致,也能解决线程安全问题
synchronized的优缺点
优点:解决了线程的安全问题。
缺点:在操作共享数据时,多线程其实是串行执行的,意味着性能低。
优化同步代码
懒汉式存在线程安全问题,如下是一个懒汉式的简单例子
public class RATMUtil {
private static RATM instance;
public static RATM getInstance(){
if (instance==null)
instance=new RATM();
return instance;
}
}
如果多个线程同时调用这个工具类,返回的instance实例可能会不同,解决的方法很简单,加锁
public class RATMUtil {
private static RATM instance;
public static RATM getInstance(){
synchronized (RATMUtil.class){
if (instance==null)
instance=new RATM();
return instance;
}
}
}
但是这样会有一个问题,instance不等于null时,多个线程还是会抢锁,同一时刻还是只能有一个线程调用该方法,所以可以将代码优化一下,如下:
public class RATMUtil {
private static RATM instance;
public static RATM getInstance(){
if (instance==null){
synchronized (RATMUtil.class){
if (instance==null)
instance=new RATM();
}
}
return instance;
}
}
注意:使用两个if判断是很有必要的。
看样子已经优化的很好了,但是这样还有可能出现问题,instance在实例化时会有很多步骤,在其中一个步骤,instance已经不为null,但还没有初始化完成,这时instance可能被别的线程拿到而造成风险,解决办法就是给变量加一个关键字volatile。
public class RATMUtil {
private static volatile RATM instance;
public static RATM getInstance(){
if (instance==null){
synchronized (RATMUtil.class){
if (instance==null)
instance=new RATM();
}
}
return instance;
}
}
死锁
不同线程分别占用对方需要的资源不放弃,都在等对方放弃自己需要的资源,就形成了线程的死锁。
诱发死锁的原因:
- 互斥条件
- 占用且等待
- 不可抢夺(不可抢占)
- 循环等待
以上4个条件同时出现就会触发死锁。
如何避免死锁
打破上述4个条件的其中一个就可以避免死锁,如下是一些方法:
- 互斥条件基本无法破坏,因为线程需要通过互斥解决安全问题。
- 考虑一次性申请所有需要的资源,这样就不存在等待问题
- 占用部分资源的线程在进一步申请其它资源时,如果申请不到,就主动释放已经占用的资源
- 可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题
Lock锁
除了使用同步代码的方式加锁,还可以使用Lock锁,Lock是jdk1.5推出的一个接口,使用接口的方式更加的灵活,本节主要讲它其中的一个实现类ReentrantLock的用法,主要使用两个方法进行加锁和解锁。
方法 | 描述 |
---|---|
lock() | 加锁 |
unlock() | 解锁 |
步骤:
- 创建Lock的实例,需要确保多个线程共用同一个Lock实例。
- 执行lock()方法,锁定对共享资源的调用
- unlock()的调用,释放对共享资源的锁定
将之前使用synchronized加锁的代码使用Lock锁改写一下
public class RATM implements Runnable{
private int balance = 10000;//将要操作的资源
private Lock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();//上锁
try {
if (balance>6000){
try {
Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
balance-=6000;
System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
lock.unlock();//解锁
}
}
//测试
public static void main(String[] args) {
RATM atm = new RATM();
Thread atm1 = new Thread(atm,"atm1");
Thread atm2 = new Thread(atm,"atm2");
atm1.start();
atm2.start();
}
}
public class TATM extends Thread{
private static int balance = 10000;//将要操作的资源
private static Lock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();//上锁
try {
if (balance>6000){
try {
Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
balance-=6000;
System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
lock.unlock();//解锁
}
}
//测试
public static void main(String[] args) {
TATM atm1 = new TATM();atm1.setName("atm1");
TATM atm2 = new TATM();atm2.setName("atm2");
atm1.start();
atm2.start();
}
}
注:Lock接口有很多的实现类,我们测试使用的是其中一个实现类ReentrantLock,本节不做深入讨论,在学习JUC时会深入了解实现类之间的不同。
synchronized与Lock的对比:
- synchronized不管是同步代码块还是同步方法,都需要结束在一对{}之后,释放对同步监视器的调用
- Lock是通过对两个方法,控制需要被同步的代码,更灵活一些
- Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高
线程通信
当我们需要多个线程来共同完成一件任务,并且我们希望他们它们有规律的执行,那么多线程之间就需要一些通信机制,可以协调它们的工作。
线程通信需要涉及Object类中的几个方法:
方法名 | 说明 |
---|---|
wait() | 线程不再活动,不再参与调度,也不会去竞争锁了,进入等待状态,等待其他线程执行notify()或notifyAll()来唤醒 |
wait(long timeout) | 线程不再活动,等待唤醒或到达指定时间后会自动唤醒 |
notify() | 唤醒优先级较高的一个线程,如果优先级都相同则会随机唤醒一个线程 ,被唤醒的线程会从当初wait()的位置继续执行 |
notifyAll() | 唤醒所有进入等待状态的线程 |
注意点
- 这几个方法必须是在同步代码块或同步方法中使用
- 这三个方法的调用者必须是同步监视器,否则会报异常
- 这三个方法声明在Object类中
接下来使用一个生产者与消费者的例子:
(1)创建产品类
public class Product {
private int productNum = 50;//产品,初始状态为50,最大不能超过100,最小不能小于0;
public synchronized void increaseProduct(){//增加产品
if (productNum<100){
productNum++;
System.out.println("生产了第"+productNum+"号产品");
notifyAll();//生产产品后就可以唤醒消费者
}else {
try {
wait();//生产数量满后就进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void reduceProduct(){//减少产品
if (productNum>0){
System.out.println("消费了第"+productNum+"号产品");
productNum--;
notifyAll();//消费者消费产品后就可以唤醒生产者生产产品
}else {
try {
wait();//如果没有产品了,消费者就会休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
(2)创建生产者
public class Producer implements Runnable{
private Product p;
public Producer(Product product){
this.p=product;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(50);//假设每50毫秒生产一个
} catch (InterruptedException e) {
e.printStackTrace();
}
p.increaseProduct();
}
}
}
(3)创建消费者
public class Consumer implements Runnable{
private Product p;
public Consumer(Product product){
this.p=product;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(25);//假设每25毫秒消费一个
} catch (InterruptedException e) {
e.printStackTrace();
}
p.reduceProduct();
}
}
}
(4)创建测试类
public class TestProduAndConsu {
public static void main(String[] args) {
Product product = new Product();
Consumer consumer = new Consumer(product);
Producer producer = new Producer(product);
//开两个消费者线程
Thread t1 = new Thread(consumer);
Thread t2 = new Thread(consumer);
//开两个生产者线程
Thread t3 = new Thread(producer);
Thread t4 = new Thread(producer);
t1.start();t2.start();t3.start();t4.start();
}
}
注:调用wait()和notify()方法时要使用同步监视器调用,比如本节中同步监视器就是this,调用本类的方法可以省略this。
wait()与sleep()的区别:
- wait()一旦执行,会释放同步监视器,而sleep()不会。
- wait()可以被唤醒,而sleep()不行,必须到达指定时间才会结束阻塞。
Callable接口实现多线程
除了使用Thread类和Runnable接口实现多线程外,还可以使用Callable接口实现多线程,该接口是jdk1.5新增的。
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
该接口是一个泛型接口。该接口与Runnable接口相比可以有返回值,并且可以通过throws处理异常,相比较起来更加灵活
步骤:
- 创建Callable接口的实现类
- 创建FutureTask类,将Callable接口的实现类传入该类中
- 创建Thread类,将FutureTask类的实例传入Thread类中(FutureTask实现了Runnable接口)
- 调用start()方法开启线程
- 使用FutureTask的get()方法获取callable接口的返回值
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i=1;i<=100;i++){
System.out.println(i);
sum+=i;
Thread.sleep(10);//增加一个时间,观察主线程阻塞状态
}
return sum;
}
}
public class TestCallable {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();//第一步
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);//第二步
Thread thread = new Thread(futureTask);//第三步
thread.start();//第四步,开启线程
try {
Integer sum = futureTask.get();//获取返回值,此时主线程(main)是阻塞状态,直至获得callable接口的返回值
System.out.println("总和:"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
直到分线程执行完所有代码并返回结果时,主线程才会继续执行。
缺点:
如果在主线程中需要获取分线程call()的返回值,则此时主线程是阻塞状态的。
线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。这就引入了线程池的,提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用。
使用线程池的优点:
- 提高程序执行的效率。(因为线程已经提前创建好)
- 提高资源的复用率。(因为执行完的线程并未销毁,而是可以继续执行其它的任务)
- 可以设置相关的参数,对线程池中的线程使用进行管理
步骤:
- 创建线程池,设置相关参数
- 执行任务
- 关闭线程池
public class MyRunnable implements Runnable{
private int i = 0;
@Override
public void run() {
for (;i<10;i++){
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i=1;i<=10;i++){
System.out.println(i);
sum+=i;
}
return sum;
}
}
测试
public class TestThreadPool {
public static void main(String[] args) {
//创建线程池,设置相关参数
ThreadPoolExecutor pool = (ThreadPoolExecutor)Executors.newFixedThreadPool(4);//指定初始的线程数量
pool.setMaximumPoolSize(8);//设置线程池最大线程数
//执行线程任务
pool.execute(new MyRunnable());//execute()方法适合执行Runnable接口的任务
Future<Integer> submit = pool.submit(new MyCallable());//submit()方法适合执行Callable接口的任务
try {
Integer sum = submit.get();//获取Callable接口的返回值
System.out.println("总和:"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}finally {
pool.shutdown();//关闭线程池
}
}
}
Lambda表达式
函数式接口
函数式接口也是接口的一种,它是一种特殊的接口,有如下特点:
- 接口内只有一个抽象方法
- 使用@FunctionalInterface注解标注(非必须)
举例:
@FunctionalInterface
public interface MyFunctionInterface {
void say(String message);
}
lambda表达式语法
(参数列表) -> {
方法体
}
如何new一个函数式接口的实例呢,有三种方式:
- 创建一个接口的实现类,然后new实现类
- 直接new接口,实现方法
- 第三种就是使用lambda表达式
举例:
@FunctionalInterface
public interface MyFunctionInterface {
void say(String message);
}
public class FunctionInterfaceTest {
public static void main(String[] args) {
MyFunctionInterface f = (message) -> {
System.out.println(message);
};
f.say("你好");
}
}
注:
- 小括号内为参数列表,接口方法有几个参数就要写几个参数,参数列表中可以省略参数类型,只写参数名
- 大括号内就是方法体,里面就写方法的实现代码
无返回值无参数
@FunctionalInterface
public interface MyFunctionInterface {
void say();
}
public class FunctionInterfaceTest {
public static void main(String[] args) {
MyFunctionInterface f = () -> System.out.println("你好");
f.say();
}
}
当接口方法没有参数时,参数列表内也不写任何参数。
注:如果方法体内就只有一条语句,可以省略大括号。
无返回值单参数
@FunctionalInterface
public interface MyFunctionInterface {
void say(String message);
}
public class FunctionInterfaceTest {
public static void main(String[] args) {
MyFunctionInterface f = message -> System.out.println(message);
f.say("你好");
}
}
如果接口方法只有一个参数,可以省略形参列表的小括号。
无返回值多参数
@FunctionalInterface
public interface MyFunctionInterface {
void say(String message1,String message2);
}
public class FunctionInterfaceTest {
public static void main(String[] args) {
MyFunctionInterface f = (message1,message2) -> {
System.out.println(message1);
System.out.println(message2);
};
f.say("你好","张三");
}
}
如果有多个参数,参数列表的小括号就不可以省略了。
有返回值无参数
@FunctionalInterface
public interface MyFunctionInterface {
String say();
}
public class FunctionInterfaceTest {
public static void main(String[] args) {
/*
MyFunctionInterface f = () -> {
return "你好";
};
*/
MyFunctionInterface f = () -> "你好";
System.out.println(f.say());
}
}
如果语句只有一条,大括号和return都可以省略。
有返回值单参数
@FunctionalInterface
public interface MyFunctionInterface {
String say(String message);
}
public class FunctionInterfaceTest {
public static void main(String[] args) {
/*
MyFunctionInterface f = (message) -> {
return message;
};
*/
MyFunctionInterface f = message -> message;
System.out.println(f.say("你好"));
}
}
同样,只有一个参数可以省略小括号,并且,如果方法体也只有一条语句,则可以省略大括号和return。
有返回值多参数
@FunctionalInterface
public interface MyFunctionInterface {
String say(String message1,String message2);
}
public class FunctionInterfaceTest {
public static void main(String[] args) {
MyFunctionInterface f = (message1,message2) -> {
System.out.println(message1);
System.out.println(message2);
return "我很好";
};
System.out.println(f.say("你好","张三"));
}
}
常用函数式接口
在java.util.function包下定义了很多函数式接口,如下介绍一些最常用的
消费型接口
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
消费一个T类型的数据
生产型接口
@FunctionalInterface
public interface Supplier<T> {
T get();
}
函数型接口
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
判断型接口
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
方法引用与构造器引用
方法引用
方法引用可以看作时lambda表达式的进一步简化,使用方法引用或构造器引用时必定可以使用lambda。
也就是说,满足特殊情况下的lambda表达式可以使用方法引用或构造器引用替换。
格式:
类(或对象)::方法名
举例:
public class FunctionInterfaceTest {
public static void main(String[] args) {
//使用lambda
Consumer<String> c1 = (message) -> System.out.println(message);
//使用方法引用
Consumer<String> c2 = System.out::println;
c1.accept("111");
c2.accept("222");
}
}
观察Consumer接口和println方法
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
public void println(String x) {
...
}
可以看到,这两个方法的返回值和形参列表是相同的,所以我们可以用println方法代替accept方法。
总结:当函数式接口的方法的形参列表、返回值与某一个类中的方法相同时,可以用方法引用的方式作为函数式接口的实现。
方法引用有如下三种方式:
类名::静态方法名
对象名::方法名
类名::实例方法名
观察如下代码:
@FunctionalInterface
public interface BiPredicate<T, U> {
boolean test(T t, U u);
}
public class FunctionInterfaceTest {
public static void main(String[] args) {
//使用lambda
BiPredicate<String,String> b1 = (s1,s2) -> s1.equals(s2);
//使用方法引用
BiPredicate<String,String> b2 = String::equals;
System.out.println( b1.test("111","111"));
System.out.println(b2.test("222","222"));
}
}
其本质还是使用了”对象::方法“,只不过引用的方法是参数对象的方法。在本例中,调用的equals方法实际是参数s1的。
满足如下关系才能使用”类名::实例方法“方式:
- 函数式接口的方法返回值与引用方法返回值相同
- 函数式接口的方法参数为n个时,引用方法的参数应为n-1个
- 函数式接口的方法至少有一个参数
- 函数式接口的方法中第一个参数作为引用方法的调用者
- 函数式接口方法的后n-1个参数类型与引用的方法参数类型相同
构造器引用
格式:
类名::new
调用无参构造
public class Person {
public Person(){}
public Person(String name){
this.name=name;
}
private String name;
}
@FunctionalInterface
public interface PersonInterface {
Person getPerson();
}
public class FunctionInterfaceTest {
public static void main(String[] args) {
//使用lambda
PersonInterface p1 = () -> new Person();
//构造器引用
PersonInterface p2 = Person::new;
}
}
调用有参构造
@FunctionalInterface
public interface PersonInterface {
Person getPerson(String personName);
}
public class FunctionInterfaceTest {
public static void main(String[] args) {
//使用lambda
PersonInterface p1 = name -> new Person(name);
//构造器引用
PersonInterface p2 = Person::new;
}
}
总结:函数式接口的方法参数列表类型要和构造器参数一致。
数组引用
@FunctionalInterface
public interface PersonInterface {
Person[] getPersons(int arrayLength);
}
public class FunctionInterfaceTest {
public static void main(String[] args) {
//使用lambda
PersonInterface p1 = name -> new Person[5];
//数组引用
PersonInterface p2 = Person[]::new;
}
}
参数为数组的长度
Stream流
使用Stream API对集合数据进行操作,就类似于使用SQL执行的数据库查询。Stream API提供了一种高校且易于使用的处理数据的方式。
Stream的一些说明
- Stream自己不会存储元素
- Stream不会改变源对象。相反,它们会返回一个持有结果的新Stream
- Stream操作是延迟执行的。这意味着它们会等到需要结果的时候才执行。即一旦执行终止操作,才会执行中间的操作,并产生结果
- Stream一旦执行了终止操作,就不能再调用其它中间操作或终止操作了。
步骤:
- 获取Stream实例
- 一系列中间操作
- 终止操作
获取Stream流对象
通过集合获取
调用集合的stream()方法或获得并行流parellelStream()
这两个方法定义在Collection接口中,所以只要实现了该接口的集合都可以获得它的Stream实例。
public class StreamTest {
public static void main(String[] args) {
List<Integer> l = new ArrayList<>();
l.add(1);l.add(2);
//获得Stream流
Stream<Integer> stream = l.stream();
Stream<Integer> integerStream = l.parallelStream();
}
}
通过数组获取
使用Arrays.stream()方法可以获得数组的Stream实例。
public class StreamTest {
public static void main(String[] args) {
Integer[] i = new Integer[]{1,2};
int[] i2 = new int[]{1,2,3};
//获得Stream流
Stream<Integer> stream = Arrays.stream(i);
//如果是基本类型的数组,也会获得Stream流,不过类型不同(IntSteam、LongStream、DoubleStream),但是方法都一样
IntStream stream1 = Arrays.stream(i2);
}
}
通过调用Stream.of()
public static<T> Stream<T> of(T t){
...
}
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);//本质还是调用了Arrays.stream()方法
}
举例:
public class StreamTest {
public static void main(String[] args) {
//获得Stream流
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
}
}
Stream流中间操作
多个中间操作可以连接起来形成一个流水线,除非流水线触发终止操作,否则中间操作不会执行任何处理!而在终止操作时一次性全部处理,称为”惰性求值“。
中间操作就是调用方法。
筛选与切片
方法 | 说明 |
---|---|
Stream<T> filter(Predicate<? super T> predicate) | 从流中排除某些元素 |
Stream<T> distinct() | 去除重复元素。使用hashCode()和equals()方法去除重复元素 |
Stream<T> limit(long maxSize) | 获取指定数量的新流(从头开始) |
Stream<T> skip(long n) | 去除前n个元素,若流中数量不足n个,则返回一个空流。与limit()互补 |
映射
方法 | 说明 |
---|---|
<R> Stream<R> map(Function<? super T, ? extends R> mapper) | 接受一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素 |
IntStream mapToInt(ToIntFunction<? super T> mapper) | 接受一个函数作为参数,该函数会被应用到每个元素上,产生一个新的IntStream |
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper) | 接受一个函数作为参数,该函数会被应用到每个元素上,产生一个新的DoubleStream |
LongStream mapToLong(ToLongFunction<? super T> mapper) | 接受一个函数作为参数,该函数会被应用到每个元素上,产生一个新的LongStream |
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper) | 接受一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流 |
排序
方法 | 说明 |
---|---|
Stream<T> sorted() | 按自然排序 |
Stream<T> sorted(Comparator<? super T> comparator) | 按比较器排序 |
注:使用第一个排序方法时一定要实现Comparable接口的类才能比较
Stream流终止操作
匹配与查找
方法 | 说明 |
---|---|
boolean allMatch(Predicate<? super T> predicate) | 检查是否匹配所有元素 |
boolean anyMatch(Predicate<? super T> predicate) | 检查是否至少匹配一个元素 |
boolean noneMatch(Predicate<? super T> predicate) | 检查是否没有匹配所有元素 |
Optional<T> findFirst() | 返回第一个元素 |
Optional<T> findAny() | 返回当前流中的任意元素 |
long count() | 返回流中的元素总数 |
Optional<T> max(Comparator<? super T> comparator) | 返回流中的最大值 |
Optional<T> min(Comparator<? super T> comparator) | 返回流中的最小值 |
void forEach(Consumer<? super T> action) | 接受一个函数,迭代每个元素 |
归约
方法 | 说明 |
---|---|
Optional<T> reduce(BinaryOperator<T> accumulator) | 将流中元素反复结合起来,得到一个值 |
T reduce(T identity, BinaryOperator<T> accumulator) | 将流中元素反复结合起来,得到一个值,identity也会参与结合 |
收集
方法 | 说明 |
---|---|
<R, A> R collect(Collector<? super T, A, R> collector) | 将流转换为其它形式。接收一个Collector接口的实现 |
注:如果想将处理好的流转换成List、Set集合,可以使用Collectors类中提供好的一些方法。
方法 | 说明 |
---|---|
toList() | 转换成list集合 |
toSet() | 转换成set集合 |
举例:
public class StreamTest {
public static void main(String[] args) {
//获得Stream流
Stream<Integer> stream = Stream.of(1,1,2,3,4,5);
List<Integer> collect = stream.distinct().collect(Collectors.toList());
}
}