java编程思想

文章目录

java编程思想

第一章 对象导论

java语言有一定的规范思想,我们程序员需要按照这样的思想去设计我们的程序。就像我们的汉语一样,如果你们使用的规范不一样,你俩说的是不同的方言,那么你俩就不能交流,因此我们为了达到交流标准,我们可以使用标准普通话来交流。java语言的规范思想其实就相当于我们的普通话,只有你的代码符合java语言规范了,那么你的代码才是一段有意义的代码,因此平常一定要研究透java编程思想。

面向对象语言OOP的五个特征

像之前最开始的汇编语言,给你一段汇编语言代码你并不知道它是干嘛的,因为它不是面向对象语言,你看汇编语言的解决问题代码,你是什么都看不懂的。而Java不一样,java对汇编语言作了抽象,java底层其实还是使用的汇编语言,java是面向对象语言OOP。也就是说你在阅读解决问题的代码的时候,其实也就是在阅读问题的表述。OOP允许根据问题来描述问题。

  • 万物皆对象。我们如果想使用程序解决一个现实中的问题,比如说一个人去超市购物,那么这个问题所有相关的事物都是对象,也就是说人去超市买东西,这里面的对象有三个,第一个是人,第二个是超市,第三个是物品。我们就可以定义三种类型的对象,每个对象都有自己的属性,行为,比如说人这个对象,它自身的属性就是年龄,身高,性别等信息,而它的行为我们可以根据具体问题具体定义,我们的问题中需要这个人做什么,我们就在人这个对象中定义什么样的方法,这样我们就可以通过调用这些方法,让人实现一些行为。
  • **程序其实就是对象的集合。**像我在公司里面做项目的时候,不管是产研项目,还是权限系统项目,这些程序都是对象的集合,这些对象通过发送消息也就是通过调用对象的方法来知道去做什么,所以多个对象的方法连续的调用,其实也就是在一步一步的做事情,也就是在处理问题。这样我们也就可以通过对象实现程序需要解决的现实问题了。
  • **对象里面包含其它对象,对象里面的其它对象就是这个对象的属性。**比如班级对象Class里面,又有学生对象Student,学生对象Student就是班级对象Class的存储。
  • **每个对象都有特定的class类型。**每个对象都是class类型的一个实例instance,而class类里面就是定义对象的属性和行为的,你可以定义其他类“可以发送什么消息给它”。其实也就是class类型里面的方法,其他对象如果想要此对象做什么事情,那么其他类直接给这个对象发送一个消息就行了,然后此对象的class类型里面需要定义对应的方法行为。
  • **子类可以接收发送给父类的信息。**其实也就是抽象,一个子类中会继承父类中所有的方法属性,这样子类中就拥有了父类中所有的方法属性。那么其他对象给父类发送的信息(也就是调用父类的方法),此信息既然能被父类接收,那么肯定也是会被子类接收的。

每个对象都有一个接口

在这里插入图片描述

上图中,类的名称是Light,这个类型的所有定义的方法就是这个类实例化一个对象之后,这个对象的接口。我们可以向Light对象发送的请求是:打开它,关闭它,将它调亮,将它调暗。

通过对象的接口,我们就能给对象发送不同的请求信息,那么我们就可以在程序中实现一些事情。

只要我这个对象包含你那个对象的所有服务,那么我这个对象就可以替换你,所以子类可以替换父类。我们知道,对象其实就是“服务提供者”,所以说,如果B对象能提供A对象的全部服务,那么B对象你就能看成A对象,以后所有出现A对象的地方你都可以替换成B对象,其实也就是子类可以替换父类。比如说我们下面的继承章节中所说的,所有的铝罐子垃圾可以替换所有的基类垃圾对象。父类不能替换子类。但是因为垃圾对象中没有铝罐子垃圾对象的服务,所以垃圾对象是肯定不能替换铝罐子垃圾对象的。

每个对象都是提供服务的

当我们开发程序或者是理解别人的程序设计时,最好的方法之一就是将对象看成是一个“服务提供者”,我们的程序本身就是向用户提供服务的,它将通过调用别的对象提供的服务去实现这一目的。因此你在开发一个程序的时候,你要做的就是去创建能够提供理想服务解决问题的一系列对象。这要这一系列对象被你构建出来了,那么你的程序就可以通过对象提供的服务去服务用户。

将对象看成是一个“服务提供者”还有一个好处,就是有助于提高对象的内聚性,达到高内聚的特性。高内聚一直是软件设计的原则之一,它的意思是一个对象中的功能不能太多,我们不能把太多功能塞到一个对象里面,我们需要给这些功能分分类,不同的功能塞到不同的对象里面,这其实就是高内聚。即每个对象都解决一类特定的问题,有一类特定的功能。

比如我们现在有购物功能,吃功能,打印功能,照明功能,那么我们就不能把这四个功能全部塞到人这个对象里面,我们需要把购物功能,吃功能塞到人这个对象里面,然后把打印功能塞到打印机对象里面,把照明功能塞到手电筒对象里面,这样就实现了我们的高内聚,即一个对象里面内聚的都是同一类功能。也因此,当年开发程序的时候,如果你将对象看成是一个“服务提供者”,你就可以自然的提高程序的内聚,达到高内聚特性,因为对于人这个对象你会想它能够提供什么服务啊:哦,它能够提供购物服务,吃服务。你肯定不会去想人可以提供照明服务。所以你看,如果你把对象当成是一个“服务提供者”,你设计出来的程序自然而然会是高内聚性的。

加入一个方法method(基类),它的输入参数是一个基类,那么我们就可以用基类的子类替换基类这个参数,为什么可以这样做呢?因为我们的对象主要是一个服务提供者,只有另外一个对象可以和当前对象提供相同的服务,那么我们就可以使用另外一个对象代替

继承

**在java中为什么会出现继承?**因为假如我们现在要创建一个class类型,但是这个class类型中的一些功能在我们之前创建的class类型里面已经有了,那么我们如果要是再在这个class类型里面写一个类似的功能(其实也就是指class类型中定义的方法),这样就显得太冗余了,代码的整体会变得很臃肿,所以为了解决这个问题,sun公司在创建java的时候就加入了继承的特点,这样我们在创建新的class类型的时候,类似的功能就不用写了,只需要继承父类(也叫做基类,超类)就可以了。

我现在对于继承的理解其实也就是抽取相同功能,就是把很多个类都要使用的功能抽到一个父类里面,然后其他若干个需要使用这个功能的类直接继承这个父类就拥有了父类的功能,每个类就不用单独再写一份与父类相同的功能了,你肯定能看出来,每个类都省去一部分相同的代码,这对我们整体代码而言会变得多么的简介。

继承关系中,子类和父类中会有相同的属性和行为,但是子类的属性和行为会在父类的基础上增多。

以后我们开发程序的时候,要使用继承进行代码解耦合,不让代码冗余,不让class类型中使用重复的代码,这样逻辑更清晰,代码也会更简洁。比如说,我们现在要开发一个程序,这个程序中有一块是和垃圾有关的,那么我们怎样设计垃圾对象呢?我们该设计几个对象呢?是使用一个垃圾class类型?还是应该使用多个垃圾class类型?是使用继承关系还是不使用继承关系?答案是,为了代码解耦,肯定是要使用继承关系的,那么该怎样继承呢?首先思考一下,我们的基类该怎么写,所有的垃圾都有的属性有重量,价值,所有的垃圾都有的功能行为有都可以被融化,分解,因此我们的基类就出来了,我们的垃圾基类中就可以有两个属性一个是重量,一个是价值,然后可以有两个行为一个是融化,一个是分解。然后再往下走,再往下细分,我们的垃圾有罐子垃圾和塑料袋垃圾,因此我们可以罐子垃圾单独创建一个class类型,然后塑料袋垃圾单独创建一个class类型,这两个class类型都会继承垃圾基类。这样一来,别的所有的具体垃圾对象就比如我们的罐子垃圾和塑料袋垃圾,直接继承我们的垃圾基类,就可以省去一大部分的冗余代码。再往下细分,我们的罐子垃圾都有的属性是颜色,但是我们的罐子有的是铁罐子,有的是铝罐子,二者的行为是不一样的,铝罐子可以被压碎,而铁罐子可以被磁化。因此我们进一步细分又可以创建两种class类型,一种是铝罐子class类型,另外一种是铁罐子class类型,这两种类型都继承我们的罐子class类型,这样铝罐子class类型和铁罐子class类型中又可以省去一部分冗余代码(罐子的颜色)。如下图:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

类的UML图如下图:

在这里插入图片描述

如上图中,通过使用继承,我们就在PotRubbish罐子垃圾类型和PlasticRubbish塑料袋垃圾类型中省略了属性weight,worth,和行为melt,analyze,这样我们的代码就解耦了,一点都不冗余了,看着非常舒服。同样的在我们的AluminumPotRubbish铝罐子垃圾类型和IronPotRubbish铁罐子垃圾类型中我们也省略了属性color,这样也能达到解耦的效果,重复的代码只在父类中写一次。

综上,可以看出来,使用基类,把相同的代码放到基类中,可以达到解耦的效果,使代码非常的简洁。

我们一个子类继承基类的时候,如果子类中什么都不做,那么子类就和基类拥有相同的属性和行为,这样没有任何的意义,既然这样你为什么不直接使用基类呢?还写一个子类干嘛呢?多次一举。所以我们如果写子类,通常有两种写法,第一种写法就是在在子类中增加一个方法,因为现有的基类不能提供这个服务,所以我们需要在子类中新加一个服务,如下图:

在这里插入图片描述
第二种方法是子类重写基类的方法,这样子类就会覆盖基类的方法,如下图:

在这里插入图片描述

为什么会出现多态?多态有什么样的好处?

我之前一直只知道有多态,只知道多态就是把一个子类new一下赋值给它的基类,然后子类中需要重写基类中的方法,然后基类变量调用基类和子类共有的功能的时候,调用的就是子类的功能。我之前只是知道这个概念。比如说,现在有一个基类

/**
	几何图形
*/
class Shape{
    publilc String area(){
        return "面积";
    }
}

然后有三个实现基类的子类,分别是圆,正方形,长方形,如下:

/**
	圆
*/
class Circle{
    public String area(){
        return "圆的面积";
    }
}

/**
	正方形
*/
class Square{
    public String area(){
        return "正方形的面积";
}
    
/**
	长方形
*/
class Triangle{
    public String area(){
        return "长方形的面积";
    }
}

什么是多态呢?你比如下面:

Shape shape = new Triangle();
shape.area();

//我们使用基类接收子类new的对象,然后调用子类中覆盖基类的方法area(),这样我们就会调用子类中的area(),这就是多态

之前我只知道什么是多态?那么为什么要使用多态呢?使用多态有什么好处呢?一个技术出来一定是存在它出来的道理的,你就比如这个多态,它的优点是什么呢?它可以让我们的代码变得好维护。怎么说呢?你比如:

现在有三个正方形,三个长方形,三个圆形,你要计算它们九个的面积之后,这些图形你要放到list集合中接收,那么你怎么接收呢?你是要写三个List集合吗,List,List,List ,然后分别调用square.area(),triangle.area(),circle.area()方法得到正方形,长方形,圆形的面积吗?这样你就会有三个list集合存储三种不同类型的对象,因为你总不能把Triangle类型的对象加入到List集合中吧!然后你还需要三种变量调用求面积的方法area(),所以这整体就会变得非常并不好维护。所以为了让我们的代码更好维护,操作更简单,我们引入了多态,我们只需要有一种集合List,因为无论是圆形,正方形,长方形它们都能提供基类几何形的服务,所以圆形,正方形,长方形,都可以存放到List集合中,然后可以通过shape.area()求圆形,正方形,长方形的面积,这样我们的代码就会变得非常好维护了。

比如说现在我们有一个方法method(正方形),如果没有多态的话有多少个导出类也即是子类,我们就需要有多少个method方法,因为我们要改变它的参数,但是如果有了多态,我们可以把子类向上转型成基类,然后只写一个method(基类)这样的形式就可以了。

策略设计模式

在一个继承体系中,假如最上层的是一个接口,然后这个接口的下层有它的一些实现类,可以当成它的子类,如下图:

在这里插入图片描述

在这里插入图片描述

如果我们现在需要执行Rubbish子类中的所有的melt()方法,我们需要写一个方法,然后把子类对象传递给这个方法,类似于这样method(PotRubbish potRubbish),然后通过参数调用melt()方法,也就是potRubbish.melt(),但是这样会存在一个问题,就是我们有多少个子类就需要写多少个method方法,是不是太麻烦了呢?而策略设计模式也就是说,我们只写一个method方法,但是这个方法可以接收所有的子类对象,最后都会调用子类对象的melt()方法,怎么实现呢?只要把参数换成最底层的接口就行了,即method(Rubbish rubbish),然后子类会自动的向上转型,你可以理解成多态。然后就能实现一个方法但是能满足每一个子类,这就是策略设计模式。其实如果最顶层的Rubbish不是接口而是基类,也是可以实现策略设计模式的,只要把method方法的参数换成是基类就行了。

为什么可以这样呢?因为对象是服务提供者,子类可以提供父类的所有的服务,因此子类可以代替父类(或父接口)。

工厂方法设计模式

工厂设计模式可以让你的程序少些很多重复的代码,可以大大提高代码的整洁性,减少冗余,降低耦合度。如果只有少量的情况,可能你并不能体会到工厂方法设计模式的优点,但是如果有大量的类似情况,你就能意识到工厂方法设计模式给你减少了多少代码量了。比如下面的这个例子:

interface Rubbish{
    //融化垃圾
    void melt();
}
interface RubbishFactory{
    //生产垃圾
    Rubbish getRubbish();
}
/**
	塑料垃圾
*/
class PlasticRubbish implements Rubbish{
    public void melt(){
        System.out.println("塑料垃圾融化");
    }
}
/**
	罐头垃圾
*/
class Pot implements Rubbish{
    public void melt(){
        System.out.println("罐头垃圾融化");
    }
}
/**
	塑料垃圾工厂
*/
class PlasticFactory implements Factory{
    public Rubbish getRubbish(){
        return new PlasticRubbish();
    }
}
/**
	罐头垃圾工厂
*/
class PotFactory implements Factory{
    public Rubbish getRubbish(){
        return new PotRubbish();
    }
}

/**
	根据传入工厂不同,生产不同的垃圾对象,并且调用垃圾对象的融化方法
*/
public class Factories{
    public static void rubbishConsumer(RubbishFactory factory){
        Rubbish rubbish = factory.getRubbish();
        rubbish.melt();
    }
    
    public static void main(String[] args){
        rubbishConsumer(new PlasticFactory());
        rubbishConsumer(new PotFactory());
    }
}

对于上面的Factories中的rubbishConsumer方法,试想一下,假如没有用这种工厂方法设计模式,假如最底层没有Rubbish垃圾接口和Factory工厂接口,那么你这个方法的参数和方法体要如何写?因为的Factory生产垃圾的工厂子类有很多,而你有没有最底层的Factory接口,所以你有多少个工厂子类是不是就应该有多少个rubbishConsumer方法,因为要适配每一个工厂子类;那假如现在Factory工厂的子类有成千上万个,你还要写成千上万个rubbishConsumer方法吗?所以对比之下你就可以发现,加一个底层的Factory接口是多么明智的事情,就算你有成千上万个Factory子类工厂,那么我们也只需要写一个rubbishConsumer方法,这可以大大提高代码的简洁性,降低代码融入度,降低代码耦合度。只要是实现了Factory接口的子类工厂,都可以作为参数传递给rubbishConsumer方法。

泛型的引入

在刚开始的时候,我们存放对象的容器比如说ArrayList,这里面我们只能存储Object类型,因为所有的类型都是Object类型的子类,所以我们可以把所有的类型都存放到ArrayList容器里面,但是存放的时候这些子类需要进行向上转型,转成Object类型,这个过程是需要耗费性能的;然后当我们从ArrayList容器里面取出元素的时候,除非我们知道我们取出的元素到底是什么具体的子类型,否则我们向下转型的时候就会存在安全隐患,我们不知道到底转哪一个具体的子类型,并且向下转型也是耗费性能的;

因此后续我们引入了泛型,就是在我们一开始创建ArrayList容器的时候,就指定它是可以存储什么类型的容器,如下:

ArrayList<正方形> list = new ArrayList<正方形>;
//我们上面创建的list容器就是一个只能存储正方形的容器,我们其他类型比如长方形,圆形在存储到list容器时,编译的时候就会报错
//这样我们在使用泛型的时候就可以避免向上转型和向下转型,可以提高我们的性能。并且也能提高安全性。因为如果容器中只能存储基类Object,那么我们从容器中取出元素的时候,就不知道到底是给它转成哪个具体子类型。

并发,并行,多线程,关于共享资源的线程安全问题

如果你的电脑只有一个处理器,是不能够实现并行的,虽然你宏观上看,一秒钟有好几个线程同时运行了,但是它们并不是真正意义上的同时运行,它们值是在1s内的前100毫秒运行A线程,然后后面运行B线程,其实每一时刻都只有一个线程运行,但是从宏观上看,1s内,A线程和B线程都运行了,所以你可能你就宏观感觉,是A线程和B线程同时运行的,其实不是,我们管这种情况叫做并发。并发是不会出现线程安全问题的,因为对于A线程和B线程的共享资源,A线程和B线程不会同时访问,只会一个访问完之后另外一个访问,所以不会有线程安全问题;

但是对于并行来说,就会出现线程安全问题了,比如现在我们的电脑有多个处理器,每个处理器可以在同一时刻处理不同的线程,这样我们就能实现真正意义上的两个线程同时运行了,我们管这种情况叫做并行。并行虽然效率比较高,但是却会出现共享资源问题,就是如果A线程要访问共享资源,B线程也要访问共享资源,那么在同一时刻访问就可能会出现问题,比如目前共享资源是a=9,A线程访问共享资源,对它做了处理让共享资源a=10,按道理来说B线程得到的共享资源应该是a=10,但是由于A,B线程是同时访问的共享资源,因此B线程得到共享资源a的时候,它仍然是a=9初始值,所以这就会出问题了。

为了解决这个问题,我们需要给共享资源加锁,就是在一个线程访问共享资源的时候,我们给这个共享资源加上一个锁,不能其它线程再访问这个共享资源,等到当前线程访问结束之后,再释放共享资源的锁,其它线程可以再次访问共享资源,这样我们就能解决多线程带来的共享资源不安全问题了。

客户/服务器技术

首先需要理解什么是服务器:系统以及存储系统信息的数据库,以及这二者所在的机器统称为服务器。

然后客户通过客户端机器上的软件来和服务器进行通信,以获取服务器上的信息,处理服务器上的信息,然后把这些信息显示在客户端的机器上。

这整个流程,就是客户/服务器技术。

**客户/服务器的问题?**客户/服务器技术只有一个服务器,但是在任何时候可能都会有成百上千个客户同时请求服务器,所以这就会给服务器端造成很大的压力。

第二章 一切都是对象

用引用操纵对象

我们在操作对象的时候,并不是直接操纵对象,而是通过对象的引用来操作对象,如下:

People people = new People();

我们使用new People()创建一个对象,然后把这个对象在内存中的地址赋值给people变量,people变量就叫做对象的引用,我们真正操作对象的时候,并不是直接操作对象,而是操纵对象的引用;就像是电视遥控器和电视机一样,我们真正想要操纵的是电视机,但是我们并没有直接操作电视机,而是通过遥控器去操纵电视机;这和我们使用对象的引用去操纵对象是同一个道理。

如果只有遥控器没有电视机,也就是只有引用但是这个引用却没有指向具体的对象的地址,这个时候向这个引用发送信息(也就是调用相关的方法)的时候,就会报运行时错误,比如:

String s;

像上面的这句代码,我们只有一个引用,但是这个引用却没有指向任何的对象,所以当我们给s发信息也就是调用s的方法的时候就会报运行时异常,如下图:

在这里插入图片描述

这就相当于我们只有遥控器,没有电视机,那么你只能操作空气,必定出错。

因此我们要想要给引用成功发送信息,必须要给引用指向具体的对象地址,如下图:

String s = new String("abc");

我们创建的对象存放到了哪个地方

我们使用new创建的对象都被放到了里面,然后**对象的引用都存放到了堆栈(也就是栈)**里面,如果堆栈里面的指针向上移了,那么表示内存释放了,表示相关的对象被从内存中销毁了;如果堆栈里面的指针向下移了,表示分配了新的内存,其实也就是我们创建了新的对象;

而向我们的基本类型,它和对象不一样,基本类型的变量存放在堆栈里面,但是和引用不一样,引用在堆栈里面存储的是对象在堆里面的地址,但是我们的基本类型的变量在堆栈里面存储的直接是基本类型变量的值。

java永远不需要销毁对象

在大多数的程序设计语言中,像C,C++我们必须要考虑对象的生命周期,什么时候创建一个对象?这个对象要存活多长时间?如果要销毁这个对象,什么时候销毁?变量的生命周期如果混乱,往往会出现很多bug,所以这会让程序员非常的头疼。但是在java中,我们必须要关心对象的生命周期,因为java中帮我们引入了垃圾回收机制,它会自动的监测对象需不需要回收,需不需要销毁。

如果垃圾回收机制算法发现一个对象不再被任何对象引用了,那么这个对象就是一个垃圾对象,会被java的垃圾回收器自动的销毁,释放内存。

比如下面的例子:

People p1 = new People();
People p2 = new People();
p2 = p1;
//刚开始我们创建了两个People对象,这两个对象会被存放到堆区,然后在堆栈中会生成两个变量引用,p1和p2,这两个引用引用的是我们在堆区生成的两个对象的地址;但是我们后面把p1的值赋值给了p2,那么现在p1和p2都是引用的堆区中的第一个People对象,因此我们第二个堆区中的People对象就不再被任何的变量引用了,这样我们java的垃圾回收器就会自动识别到这个对象是一个垃圾对象,然后将其销毁。

基本类型的转型提升

首先我们java中一共有八种基本类型,byte(占一个字节),short(占两个字节),int(占四个字节),float(占四个字节),long(占八个字节),double(占八个字节),char(占一个字节),boolean;

其中哪些比int类型小的类型,也就是byte,short,char类型,在进行运算的时候会自动转换成int类型,如下:

char c = 'f';
byte b = 1;
short s = 2;
int result = c+b+s;
//在char,byte,short类型运算的时候,会自动的类型提升转换为int类型,其中'f'主要是取的它的对应的ASCII码的值

小的类型转换到大的类型不会出现什么问题,但是如果是向下转型,即大的类型转换为小的类型,比如说long类型向下转型为int类型,这就可能会造成数据丢失了,是会存在问题的,因为你的long类型是8个字节,而你的int类型是4个字节,加入long类型存储的数据很大的话,占了六个字节,那么你把long类型的数据向下转型为int类型就会丢失两个字节的数据,因此肯定会出现问题。

因此我们基本类型运算的时候,结果都是转型成最大的那个类型,比如说int类型数据和long类型数据运算,我们最终会把结果转型成long数据,这样才不会造成数据丢失。

第三章 初始化与清理

this关键字

当我们调用一个对象的方法的时候,java会默认把当前对象的引用作为第一个参数传递,这个过程你是看不到的。然后在class类型的方法中我们可以使用this关键字代表当前调用此方法的对象的引用,如下:

class People{
    private int age;
    private String sex;
    People(int age,String sex){
        this.age = age;
        this.sex = sex;
    }
    public void detailMessage(){
        System.out.println("当前年龄:"+this.age+",性别:"+this.sex);
    }
    
    public void test(){
        detailMessage();
    }
}

//外部调用,使用p.detailMessage()调用对象方法的时候,看似没有参数,但是其实我们在运行的时候java会帮我们默认加一个参数,他会把当前对象的引用也就是p的值传给detailMessage()方法的第一个参数,然后在People这个class类型的detailMessage()方法中就可以使用this关键字当做调用detailMessage()方法的对象了
People p = new People(18,"男");
p.detailMessage();

//class类型内部的方法调用不用特别写this关键字,java运行时会帮我们自动加,这里在外部调用test()方法的时候,默认会把对象的引用也就是p传递给test()方法的第一个参数,然后test()方法里面可以通过this关键字当做我们调用test()方法的对象的引用,然后test()方法内部调用detailMessage()方法的时候,前面不用写this关键字,也就是不用写成this.detailMessage()的形式,运行的时候这个this会给我们默认加上
People p = new People(18,"男");
p.test();

在构造器中可以通过this调用构造器,如下:

class People{
    int age;
    String sex;
    People(int age){
        this.age = age;
    }
    People(String sex){
        this.sex = sex;
    }
    People(int age,String sex){
        this(age);
        //this(sex);  只能调用一次构造器,并且构造器代码要放在第一行,否则会报错
        this.sex = sex;
    }
}

//在外部创建对象
People p = new People(18,"男");

对于java来说,除了在构造器中可以调用构造器,在其它任何方法中都不能调用构造器;并且在构造器里面只能调用一次构造器且必须放在开头第一行,否则会报错。

static关键字

理解了this关键字之后,我们就可以对static静态方法做进一步的理解了,static静态方法就是没有this关键字的方法,在static静态方法当中,你不能够调用其他的非静态方法,因为static静态方法没有this,它不知道该调用哪个对象的非静态方法;

static修饰的静态方法,可以在对象没有实例化之前通过class类型来调用,并且所有的class类型实例化的对象共用同一个static静态方法。

同样的道理,对于static修饰的静态属性,也可以使用class类直接调用,在class类型没有具体实例化对象之前,就可以通过class类调用,并且所有class类型实例化的对象共用同一份static静态属性。

**我们的static修饰的静态属性如何初始化呢?**可以使用静态代码块对我们的static修饰的静态属性初始化,如下:

class People{
    static int age;
    static String sex;
    static{
        age = 18;
        sex = "男";
    }
 	void test(){
        System.out.println("年龄:"+age+",性别:"+sex);
    }
}

//外部调用,会输出“年龄:18,性别:男”
People p1 = new People();
p1.test();

成员初始化

**Java会尽力保证所有变量在使用之前都能够得到恰当的初始化。**如果是方法的局部变量,如果不初始化,java会报编译时的错误,如下:

public int test(){
    int i;
    i++;
}
//上面i++这句代码会报编译时错误,提示你要初始化

在这里插入图片描述

如果是类的属性没有初始化,加入这个属性的类型是八种基本类型,那么java会默认给它一个默认值,但假如这个属性的类型是对象引用,那么这个引用的默认值就是null,如下:

class People{
    int age;   //默认值0
    long height;  //默认值0
    Parent parent;   //这里的引用类型没有初始化,它的值会是null
}

类加载的时候静态属性,非静态属性,和方法加载的顺序

new一个对象初始化的时候,首先加载的是它的static静态属性,如果没有赋值的话会自动赋初始值;然后加载的是它的非static静态属性,如果没有赋值的话也会自动赋初始值;再然后才是加载方法;并且,即使属性是在方法的下面,也是先加载属性;加载属性的顺序是先加载static静态属性,按顺序加载,在前面的static静态属性先加载,然后再加载非static静态属性,仍然是按顺序加载,前面的先加载;具体示例代码如下:

class Bowl{
    Bowl(int marker){
        print("Bowl(" + marker + ")");
    }
    void f1(int marker){
        print("f1(" + marker + ")");
    }
}

class Table {
    static Bowl bowl1 = new Bowl(1);
    Table() {
        print("Table()");
        bol2.f1(1);
    }
    void f2(int marker){
        print("f2(" + marker + ")");
    }
    static Bowl bowl2 = new Bowl(2);
}

class Cupboard {
    Bowl bowl3 = new Bowl(3);
    static Bowl bowl4 = new Bowl(4);
    Cupboard(){
        print("Cupboard()");
        bowl4.f1(2);
    }
    void f3(int marker){
        print("f3(" + marker + ")");
    }
    static Bowl bowl5 = new Bowl(5);
}

public class StaticInitialization{
    public static void main(String[] args){
        print("Creating new Cupboard() in main");
        new Cupboard();
        print("Creating new Cupboard() in main");
        new Cupboard();
        table.f2(1);
        cupboard.f3(1);
    }
    static Table table = new Table();
    static Cupboard cupboard = new Cupboard();
}

运行main方法输出结果如下:

Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)

运行main方法会加载StaticInitialization类,注意这个时候只是运行StaticInitialization类的main方法并没有初始化这个类,所以它的非静态的属性是不会初始化的,只会初始化它的static静态属性,因为这里StaticInitialization类恰巧没有非静态属性,所以你看不出来效果,但假如这里有static非静态属性,因为我们只是运行main方法,没有对StaticInitialization类进行对象初始化,所以静态属性是不会被加载的,静态属性只有我们创建对象的时候才会被加载,而非静态属性在我们第一次加载class模板的时候可以被加载(看书对比着更好理解一点)。而加载StaticInitialization类的时候会加载table,cupboard类,一层一层的加载,最后在运行main方法之前,我们就已经把所有的class类加载完毕了。

可变参数列表

java中方法的参数一般我们都是特定的,有几个参数我们就定义几个参数,像下面这样:

public void test(String name,Integer age){
    ......
}
//像上面的这个方法,它的参数就是特定的,只有两个参数,一个是name姓名,另外一个是age年龄

而有一些方法,我们希望它的参数不是特定的,我们希望它是可变的,比如说有时候是两个,有时候是三个,我们要怎么定义呢?可以这样,如下:

public class AbleChange {
    /**
    	使用Integer... args代表可变参数,这样调用这个方法的那个地方可以随便传递参数,想要传递几个就传递几个,最后
    	会用一个args数组接收。并且调用的那个地方也可以直接传递一个数组;
    */
    static public void test(Integer... args){
        for (int i = 0; i<args.length; i++){
            System.out.println(args[i]);
        }
    }

    static public void test2(String... args){
        for (int i = 0; i<args.length; i++){
            System.out.println(args[i]);
        }
    }

    public static void main(String[] args) {
        test(1,2,3,4);
        test2(new String[]{"ab","cd"});
    }
}

运行main方法之后的输出如下图:

在这里插入图片描述

final关键字

作为类的常量值,供外部调用

//作为类的常量值的时候,通常是public,static,final三者配合使用,final的作用是说明此属性必须要初始值并且值不能更改,static的作用是类中只加载一次这个属性,public的作用是让外部包的其它的地方可以访问,如果final和static配合使用了,那么属性名字必须要用大写,这是规范
public class Constant{
    public static final String NAME = "zs";
    public static final int AGE = 18;
    public final String sex = "男";
}

如果用在方法中,可以不赋初始值,但是它的值一旦被定义就不能改变了

public void test(){
    final int i;
    i = 0;
}

如果用在一个引用身上,这个属性对应的引用值不能被更改,但是对象内部的属性仍然可以被更改。

final如果修饰方法的参数,那么你在方法的内部就不能够修改此参数

public void test(final int a,final People people){
    a = 4; //这句代码是错误的,因为参数用final进行修饰了,所以你就不能够修改a的值了
    people = new People(); //这句代码也是错误的,因为参数使用final进行修饰了,所以你就不能再更改引用的值了
}

final如果修饰方法的时候,主要是不让此方法被它的子类重写

在这里插入图片描述

父类中就不能够再重写method方法了,如下图:

在这里插入图片描述

final修饰方法主要是为了保证子类不乱写父类中的某些方法。

final修饰类的话,那么此类就不允许继承了

在这里插入图片描述

第四章 类型信息(反射)

什么时候使用反射

**运行时类型信息使得你可以在程序运行时发现和使用类型信息。**比如下面的例子:

/**
	几何图形
*/
class Shape{
    publilc String area(){
        return "面积";
    }
}

然后有三个实现基类的子类,分别是圆,正方形,长方形,如下:

/**
	圆
*/
class Circle{
    public String area(){
        return "圆的面积";
    }
}

/**
	正方形
*/
class Square{
    public String area(){
        return "正方形的面积";
}
    
/**
	长方形
*/
class Triangle{
    public String area(){
        return "长方形的面积";
    }
}
    
/**
	写一个容器可以容纳所有子类
*/    
public class Test{
    public static void main(String[] args){
        //我们的shapeList使用基类作为泛型的具体类型,因此可以容纳所有的子类
        List<Shape> shapeList = Arrays.asList(
        	new Circle(),new Square(),new Triangle()
        );
        for(Shape shape : shapeList){
            shape.draw();
        }
    }
}

那么我们什么时候我们会用到反射获取运行时的具体类的信息呢?假如现在我们有一个需求,需要用某个方法来旋转容器中的所有的图形,但因为我们对圆形的旋转是没有意义的,所以我们想跳过圆形。那么从容器中取出对象之后,我们就可以通过反射来获取这个对象的运行时的信息,看看它是不是圆形对象,如果是的话,我们就不进行旋转。这就是使用反射的时机。

Class对象

自定义的class类型是一个Class对象

我们自定义的class类型是一个Class对象,每当我们编写并且编译了一个新的类的时候,就会产生一个新的Class对象,如下图:

在这里插入图片描述

并且这个Class对象是被保存在一个同名的.class文件中的。这个Class对象会在第一次需要用到的时候被JVM加载,并且只加载一次。

JDK中的Class模板

既然我们自定义的所有的class类型模板都是一个Class对象,那么我们的JDK中肯定是有一个Class类型模板的,如下图:

在这里插入图片描述

java中的特殊对象和常规对象

java中的特殊对象只有一种,就是Class对象;Class对象其实就是我们自定义的一个class模板,并且我们的Class对象只在JVM中加载一次,所以对于我们自定义的一个class模板,在程序中只有一个与其对应的Class对象;而我们的常规对象也就是使用Class对象生成的对象,它可以有多个,常规对象其实就是我们平常使用我们的class类型模板生成的对象。

类加载器首先检查这个类的Class对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名查找.class文件,例如,某个附加类加载器可能会在数据库中查找字节码。在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良Java代码,这是Java中用于安全防范的措施之一。**java程序在它开始运行之前并非完全加载,其各个部分都是在必需时才加载的。**如下:

class Candy{
    static{
        System.out.println("Loading Candy");
    }
}

class Gum{
    static{
        System.out.println("Loading Gum");
    }
}

class Cookie{
    static {
        System.out.println("Loading Cookie");
    }
}

public class SweetShop {
    public static void main(String[] args) {
        System.out.println("inside main");
        new Candy();
        System.out.println("After creating Candy");
        try {
            //加载Class对象,参数是一个字符串,必须是class类模板的全限路径名包括包名,这里如果Class.forName()找不到你要加载的类对象,就会抛出异常ClassNotFoundException
            Class.forName("Gum");
        } catch (ClassNotFoundException e){
            System.out.println("Couldn't find Gum");
        }
        System.out.println("After Class forName(\"Gum\")");
        new Cookie();
        System.out.println("After creating Cookie");
    }
}

//运行main方法之后的输出结果如下
inside main
Loading Candy
After creating Candy
Couldn't find Gum
After Class forName("Gum")
Loading Cookie
After creating Cookie
//我们的静态代码块是随着class类的第一次加载而加载的,所以从我们的输出结果可以看出来,我们的Candy,Gum,Cookie类都不是在我们刚刚定义完class模板加载的,而是我们在main方法里面第一次需要使用它们的时候进行第一次加载的

当我们想要在运行时使用类型信息的时候,就必须先获取到对应的Class对象的引用。有两种简便的方法,第一种可以通过Class.forName()来获取Class对象的引用,它的参数是对应的class模板的全限路径名,比如我们现在自定义了一个类型模板class Student{…},那么我们就可以通过Class.forName(org.wangxuan.Student)来获取我们的Class对象的引用;第二种方法是,如果我们已经有了一个常规的对象,就比如说我们现在已经创建了一个Student对象,Student stu = new Student();那么我们可以通过调用常规对象的getClass()方法来获取到创建常规对象的Class对象的引用,即通过stu.getClass()获取。Class类型模板中包括许多常用的方法,比如说你可以获取当前Class对象是否为接口?你也可以获取到当前Class对象的不含包名的类名和全限定名;你也可以获取到Class对象的父Class对象等等等等。如下:

interface HasBatteries{ }
interface Waterproof{}
interface Shoots{}

class Toy{
    Toy(){}
    Toy(int i){}
}

class FancyToy extends Toy implements HasBatteries,Waterproof,Shoots{
    FancyToy(){
        super(1);
    }
}

class ToyTest{
    static void printInfo(Class cc){
        System.out.println("Class name: " + cc.getName() + " is interface? [" + cc.isInterface() + "]");
        System.out.println("Simple name: " + cc.getSimpleName());
        System.out.println("Canonical name: " + cc.getCanonicalName());
    }

    public static void main(String[] args) {
        Class c = null;
        try{
            c = Class.forName("wangxuan3.FancyToy");
        } catch (ClassNotFoundException e){
            System.out.println("Can't find FancyToy");
            System.exit(1);
        }
        printInfo(c);
        for (Class face : c.getInterfaces())
            printInfo(face);
        Class up = c.getSuperclass();
        Object obj = null;
        try{
            //使用Class对象进行实例化常规对象的时候,要求我们的自定义class类型模板当中必须有无参构造器
            obj = up.newInstance();
        } catch (InstantiationException e){
            System.out.println("Cannot instantiate");
            System.exit(1);
        } catch (IllegalAccessException e){
            System.out.println("Cannot access");
            System.exit(1);
        }
        printInfo(obj.getClass());
    }
}

//运行main方法之后的输出结果
Class name: wangxuan3.FancyToy is interface? [false]
Simple name: FancyToy
Canonical name: wangxuan3.FancyToy
Class name: wangxuan3.HasBatteries is interface? [true]
Simple name: HasBatteries
Canonical name: wangxuan3.HasBatteries
Class name: wangxuan3.Waterproof is interface? [true]
Simple name: Waterproof
Canonical name: wangxuan3.Waterproof
Class name: wangxuan3.Shoots is interface? [true]
Simple name: Shoots
Canonical name: wangxuan3.Shoots
Class name: wangxuan3.Toy is interface? [false]
Simple name: Toy
Canonical name: wangxuan3.Toy

除了使用Class.forName()得到Class对象的引用,也可以直接使用.class来得到Class对象的引用,就比如Student.class就是获取常规对象Student的Class对象的引用。这两种获取Class对象的引用的区别是什么呢?Class.forName()会加载类模板,而Student.class不会加载类模板。如下:

class Initable{
    static final int staticFinal = 47;
    static{
        System.out.println("我输出了");
    }
}

class Initable2{
    static final int staticFinal2 = 48;
    static{
        System.out.println("等一会");
    }
}

class Test{
    public static void main(String[] args){
        Class initable = Class.forName("org.wangxuan.Initable");
        Class initable2 = Initable2.class;
    }
}

//输出信息
我输出了

使用泛化的Class对象的引用与不使用泛化的Class对象的引用哪个更好?

使用泛化Class对象的引用的好处

先来看一下不使用泛型的Class对象的引用,如下:

//因为我们没有对Class对象使用泛型,所以我们的Class对象的引用可以引用任何类型的Class对象
public class GenericClass {
    public static void main(String[] args) {
        Class intClass = int.class;
        System.out.println(intClass);
        intClass = double.class;
        System.out.println(intClass);
    }
}
//输出结果
int
double

再来看一下使用泛型的Class对象的引用,如下:

在这里插入图片描述

可以看出,如果我们使用了泛化的Class对象的引用,那么我们在编译期间就可以发现我们程序的异常,这就可以避免在运行时由于我们的疏忽而产生其他Class对象引用的错误。

使用泛化Class对象引用的规范

如果我们想要让我们的Class对象的引用不指名具体的Class对象类型,也就是说无论是什么样的Class对象都可以引用,一种方式是直接如下定义:

//不使用泛型,那么这个Class对象的引用就可以引用任何的Class对象类型
Class allClass = int.class;
allClass = double.class;
allClass = student.class;

//上面这种形式等价于下面使用?通配符的形式,使用泛型,加?通配符也可以让我们的Class对象的引用指向任何Class类型对象的引用
Class<?> allClass = int.class;
allClass = double.class;
allClass = student.class;

但是在JDK5中,Class<?>是优于平凡的Class,即便它们是等价的,并且平凡的Class如你所见,不会产生编译器警告信息。

Class<?>的好处是它表示你并非是碰巧或者由于疏忽,而是用了一个非具体的类引用,你就是选择了非具体的版本。所以,以后我们要想要让我们的Class对象的引用可以是任何类型的Class对象,

我们要使用Class<?>的泛化形式去定义我们的Class对象的引用。

如果我们想要让泛型的限制再紧一些,我们可以使用Class<? extends Parent>的形式,如下:

//我们这里泛化的Class对象是Class<? extends Number>,它表示我们的Class对象的引用可以是任何的Number父类或者其所有的子类,如果是的话不会有编译时异常;但是如果不是的话是会出现编译时异常的,这样就可以防止由于我们的疏忽在运行时产生异常。比如如果没有使用泛型的话,不会出现编译时异常,那么我们如果由于疏忽手动的把某个类型的Class对象的引用给修改了,编译时是正常的,但是运行时可能会出错,所以这就很麻烦了。而对Class对象引用的泛化可以避免出现这个问题,因为它在编译期间就会帮助我们自动检查,如果有问题编译时就会报错。
public class BoundedClassReferences{
    public static void main(String[] args){
        Class<? extends Number> bounded = int.class;
        bounded = double.class;
        bounded = Number.class;
    }
}

向下转型时使用instanceof判断所属类型是否正确,避免出现转型异常

使用instanceof向下转型的时机:需要子类先转父类,然后父类再转子类,父类做判断能否向下转型为对应的子类的时候才会用到instanceof关键字。比如说现在有一个父类Parent,然后有几个子类Children1,Children2,Children3,我们有一个父类容器,把我们的三个子类都放到父类容器的时候都会向上转型为父类,虽然父类容器中现在都是父类Parent,但是这三个元素本质上是不同的东西,我们从父类容器中取出一个元素的时候,它可能是Children1,也可能是Children2,也可能是Children3,我们父类Parent向下转型的时候不能瞎转,因为如果这个元素明明不是Children3,我们偏转成Children3,就会出现转型异常。因此这个时候我们才会用到instanceof。

当我们有一个父类容器,我们想要取出来它里面的具体的子类类型,并且进行向下转型的时候,为了避免转型错误,我们可以使用instanceof关键字在转型前先进行一次判断,格式obj常规对象 instanceof 具体的Class类型比如Student,但不能是Class对象的引用比如说Student.class如下:

/**
	几何图形
*/
class Shape{
    publilc String area(){
        return "面积";
    }
}
/**
	圆
*/
class Circle{
    public String area(){
        return "圆的面积";
    }
}

/**
	正方形
*/
class Square{
    public String area(){
        return "正方形的面积";
}
    
/**
	长方形
*/
class Triangle{
    public String area(){
        return "长方形的面积";
    }
}
    
//在把基类向下转型为具体的子类之前,因为我们并不知道这个基类具体是哪一个子类,所以我们转型之前要使用instaceof关键字首先判断一下我们的基类所属子类的类型,然后再进行转型,否则会出现转型异常
class Test{
    public static void main(String[] args){
        List<Shape> list = new ArrayList<Shape>();
        list.add(new Circle());
        list.add(new Square());
        list.add(new Triangle());
        for(int i = 0; i < list.length; i++){
            if(list.get(i) instanceof Circle){
                Circle circle = (Circle)list.get(i);
            } else if(list.get(i) instanceof Square){
                Square square = (Square)list.get(i);
            } else{
                Triangle triangle = (Triangle)list.get(i);
            }
        }
    }
}

注意,使用instanceof的时候只可将其与命名类型进行比较,比如说我们自定义的类型Cricle,而不能与Class对象的引用做比较,比如说与Cricle.class做比较。

Class类型的Class.isInstance(Object obj)方法也可以比较我们的一个常规对象是否属于某个Class对象引用类型,如下:

Circle.class.isInstance(circle) //这里就是比较我们的常规对象circle是否属于Circle Class对象引用类型

为什么需要使用反射?

**什么是RTTI?**首先需要理解什么是RTTI,RTTI就是它可以让你在运行时获取对象的类型信息。但是有一个前提,我们的这个类型信息必须要在编译期间被编译器编译了。就比如说我们现在有一个程序,我们在程序里面自己定义了一个class类型模板,即class Student{…},然后我们在创建了一个student对象Student student = new Student();那么在运行的时候,我们就可以通过RTTI获取到student对象的类型信息,因为在编译期间我们已经编译了Student类型模板了。但是如果我们的student对象是从另外一个程序得到的,或者是从本地磁盘读取到的,那么我们在运行的时候就不能通过RTTI得到student对象对应的class类型模板信息,因为在编译期间它没有被编译,那这个时候要怎么办呢?我们该怎么获取到这个对象的class类型信息呢?这个时候可以使用反射。

**反射是什么?**要认识到反射机制并没有什么神奇之处。当通过反射与一个位置类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类(就像RTTI那样)。在用它做其它事情之前必须先加载那个类的Class对象。因此,那个类的.class文件对于JVM来说必须是可获取到:要么在本地机器上,要么可以通过网络取得。所以RTTI和反射之间真正的区别只在于,对RTTI来说,编译器在编译时打开和检查.class文件,而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。

通过反射在运行时根据方法名调用方法

在运行的时候我们假设现在从另外一个程序中获取到了一个对象,现在我们知道这个对象中有一个方法的名字,并且知道这个方法的参数,如何通过反射调用这个对象中的方法呢?甚至是调用私有方法?如下:

public class Reflect {
    private void aa(){
        System.out.println("无参的aa方法执行了");
    }
    private void aaa(String s,Integer integer){
        System.out.println("带参数的aaa方法执行了");
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
        Reflect reflect = new Reflect();
        Class<? extends Reflect> reflectClass = reflect.getClass();

        Method aa = reflectClass.getDeclaredMethod("aa");
        //通过反射获取方法之后,在通过invoke调用之前,需要设置安全性检查为true
        aa.setAccessible(true);
        aa.invoke(reflect);

        Method aaa = reflectClass.getDeclaredMethod("aaa", String.class, Integer.class);
        //通过反射获取方法之后,在通过invoke调用之前,需要设置安全性检查为true
        aa.setAccessible(true);
        aaa.invoke(reflect,"string",1);
    }
}


//输出结果
无参的aa方法执行了
带参数的aaa方法执行了

通过反射在运行时根据属性名修改属性值

在运行的时候我们假设现在从另外一个程序中获取到了一个对象,现在我们知道这个对象中有一个属性的名字,如何通过反射重新设置对象中的这个属性的值呢?如下:

public class Reflect {
    private String name;
    private Integer age;
    Reflect(String name,Integer age){
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Reflect{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Reflect reflect = new Reflect("葫芦娃",7);
        Class<? extends Reflect> aClass = reflect.getClass();
        System.out.println("修改之前->>"+reflect);

        Field name = aClass.getDeclaredField("name");
        //无论是通过反射操作方法还是通过反射操作域,在获得方法对象或者是域对象之后,在操作之前都需要把安全性检查设置为true
        name.setAccessible(true);
        name.set(reflect,"熊出没");

        Field age = aClass.getDeclaredField("age");
        //无论是通过反射操作方法还是通过反射操作域,在获得方法对象或者是域对象之后,在操作之前都需要把安全性检查设置为true
        age.setAccessible(true);
        age.set(reflect,2);

        System.out.println("修改之后->>"+reflect);
    }
}

//输出结果
修改之前->>Reflect{name='葫芦娃', age=7}
修改之后->>Reflect{name='熊出没', age=2}

第五章 注解

注解的作用

注解主要是给我们提供额外的信息的。注解和注释一样,可以为我们提供额外的信息,但是注解和注释的区别是,我们可以通过代码获取到注解为我们提供的信息。

注解还可以提供一些我们java代码无法表达的信息,比如可以使用@Override注解检查重写方法的正确性。

注解的基本语法

//定义注解的关键字和定义接口的关键字差不多,只不过多了一个@符号,即@interface
//注解里面只有属性,没有方法,但是注解的属性与我们类里面的属性不同的点在于它需要加一个小括号,如id()
//可以通过default给注解的属性加上一个默认值
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase{
    public int id();
    public String description() default "no description";
}

//怎样通过注解对象获取到注解里面的属性
UserCase uc = method.getAnnotation(UseCase.class);
uc.id() //得到注解内属性id()的值
uc.description() //得到注解内属性description()的值

java中自带的其中注解

java中的三种标准注解如下:

  • @Override,表示当前修饰的方法是重写的父类中的方法,如果重写的时候方法名字不对,会在编译时报错。
  • @Deprecated,此注解修饰的元素表示过时元素,在别的地方使用时会自动加一个横线表示已经过时。
  • @SuppressWarnings,如果加了此注解,编译期间的警告信息会消失。

四种元注解,专门修饰注解的注解,如下:

  • @Target,表示该注解可以用在什么地方,可能的ElementType参数包括field域,method方法,type类等。
  • @Retention,表示需要在什么级别保存该注解信息。可选的RetentionPolicy参数包括,source表示源码中有此注解但是编译的时候该注解将会丢失,class表示编译的时候会有此注解但是运行的时候该注解会丢失,runtime表示运行的时候该注解也不会丢失,我们可以在运行时通过反射得到注解的相关信息。
  • @Documented,将此注解包含在Javadoc中
  • @Inherited,允许子类继承父类中的注解

虽然java自带的有七种注解,但是我们在实际开发中一般不会直接使用它的这七种注解。在实际开发中,大多数时候,程序员主要是定义自己的注解,并通过反射编写自己的注解处理器,从而获取我们的注解的相关信息,用于达成某个目的。

编写注解处理器

如果没有用来读取注解的工具,那注解也不会比注释更有用。使用注解的过程中,很重要的一个部分就是创建与使用注解处理器。而我们的java可以通过反射去自定义我们自己的注解处理器。

例子:

//首先有一个自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase{
    public int id();
    public String description() default "no description";
}

//然后有一个被自定义注解修饰的类
public class PasswordUtils {
    @UseCase(id = 47, description = "Passwords must contain at least one numeric")
    public boolean validatePassword(String password){
        return (password.matches("\\w*\\d\\w*"));
    }
    
    @UseCase(id = 48)
    public String encryptPassword(String password){
        return new StringBuilder(password).reverse().toString();
    }
    
    @UseCase(id = 49, description = "New passwords can't equal previously used ones")
    public boolean checkForNewPassword(List<String> prevPasswords,String password){
        return !prevPasswords.contains(password);
    }
}

下面是一个非常简单的注解处理器,我们将用它来读取PasswordUtils类,并使用反射机制查找@UseCase标记。我们为其提供了一组id值,然后它会列出在PasswordUtils中找到的用例,以及缺失的用例。使用反射创建注解处理器,如下:

public class UseCaseTracker{
    public static void trackUseCases(List<Integer> useCases, Class<?> cl){
        for(Method m : cl.getDeclareMethods()){
            UseCase uc = m.getAnnotation(UseCase.class);
            if(uc != null){
                System.out.println("Found Use Case:" + uc.id() + " " + uc.description());
                useCases.remove(new Integer(uc.id()));
            }
        }
        for(int i : useCases){
            System.out.println("Warning: Missing use case -" + i);
        }
    }
    public static void main(String[] args){
        List<Integer> useCases = new ArrayList<Integer>();
        Collections.addAll(useCases,47,48,49,50);
        trackUseCases(useCases, PasswordUtils.class);
    }
}

//输出结果
Found Use Case:47 Passwords must contain at least one numeric
Found Use Case:48 no description
Found Use Case:49 New passwords can't equal previously used ones
Warning: Missing use case-50

编写注解处理器的另一个例子–>通过注解处理器获取注解里面对应的数据库信息

比如现在我们有一个javabean实体类,我们如何能够在运行的时候知道这个javabean实体类对应的数据库表是哪一个?我们如何能知道javabean实体类的属性在mysql数据库表中对应的字段名字和字段的长度,以及字段的约束条件呢?我们可以把这些信息放到注解里面,在编译期间直接把注解注释到javabean实体类的上面和javabean对应的属性的上面,就相当于是我们在编译时把这些信息都告诉编译器了,在运行的时候我们就可以通过反射机制去编写一个注解处理器去读取这些信息。具体实现过程如下:

/**
 * DBTable注解主要是用来增加javabean对应的mysql数据库的表名信息的
 * */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface DBTable{
    //javabean实体类对应的mysql表名
    String name() default "";
}

/**
 * @Constraints注解主要是用来说明我们的javabean中的属性对应的mysql数据库表字段的约束信息的,此注解会被嵌套到其它注解当中
 * */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Constraints{
    //javabean属性对应的mysql字段是否为主键
    boolean primaryKey() default false;
    //javabean属性对应的mysql字段是否可以为空
    boolean allowNull() default true;
    javabean属性对应的mysql字段是否为唯一的
    boolean unique() default false;
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface SQLString{
    //javabean属性String类型字段对应的mysql数据库表中的String类型的字段的长度
    int value() default 0;
    //javabean属性String类型字段对应的mysql数据库表中的String类型的字段的名字
    String name() default "";
    //javabean属性String类型字段对应的mysql数据库表中的String类型的字段的约束条件。
    //可以看出这里的注解属性和其它的基本类型属性一样,类型都是直接写注解的名字,赋默认值的时候直接写@Constraints就可以实现赋值了
    Constraints constraints() default @Constraints;
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface SQLInteger{
    //javabean属性Integer类型字段对应的mysql数据库表中的Integer类型的字段的名字
    String name() default "";
    //javabean属性Integer类型字段对应的mysql数据库表中的Integer类型的约束条件
    Constraints constraints() default @Constraints;
}

/**
 * Member实体类是我们java中的DO实体类
 * */
@DBTable(name = "Member")
class Member{
    
    @SQLString(value = 30, name = "firstName")  //此注解说明我们javabean的firstName属性在mysql数据库表中对应的字段名字是firstName,长度为30,约束为约束注解中的默认约束值
    private String firstName;

    @SQLString(value = 50, name = "lastName") //此注解说明我们javabean的lastName属性在mysql数据库表中对应的字段名字是lastName,长度为50,约束为约束注解中的默认约束值
    private String lastName;

    @SQLInteger(name = "age") //此注解说明我们javabean的age属性在mysql数据库表中对应的字段名字是age,约束为约束注解中的默认约束值
    private Integer age;

    @SQLString(value = 30, constraints = @Constraints(primaryKey = true))
    private String handle;
}


public class TableCreator {
    public static void main(String[] args) throws Exception {
        Class<?> cl = Class.forName("wangxuan9.Member");
        //得到修饰Member Class对象的@DBTable注解
        DBTable dbTable = cl.getAnnotation(DBTable.class);
        if(dbTable != null){
            System.out.println("Member类所对应的mysql表的名字是:"+dbTable.name());
        } else {
            System.out.println("Member类所对应的mysql表的名字是:"+cl.getSimpleName());
        }

        //得到Member Class对象中的所有的属性
        Field[] declaredFields = cl.getDeclaredFields();
        for (Field field : declaredFields){
            //获取当前的属性名字
            String fieldName = field.getName();

            //得到修饰属性的所有的注解,注意此时得到的是最底层的注解Annotation对象,类似于Object父类对象,我们后续需要向下转型
            Annotation[] declaredAnnotations = field.getDeclaredAnnotations();
            if(declaredAnnotations.length < 1){
                continue;
            }

            //因为我们这里修饰属性的注解只有一个,所以就直接取出来了
            Annotation declaredAnnotation = declaredAnnotations[0];

            //判断是否为String类型的注解
            if(declaredAnnotation instanceof SQLString){
                SQLString sqlString = (SQLString)declaredAnnotation;
                System.out.println("javabean中的" + fieldName + "字符串属性,在mysql表中的名字是" + sqlString.name() + ",长度是" + sqlString.value()
                        + ",约束条件为" + sqlString.constraints());
            }

            //判断是否为Integer类型的注解
            if(declaredAnnotation instanceof SQLInteger){
                SQLInteger sqlInteger = (SQLInteger)declaredAnnotation;
                System.out.println("javabean中的" + fieldName + "整型属性,在mysql表中的名字是" + sqlInteger.name()
                        + ",约束条件为" + sqlInteger.constraints());
            }

        }
    }
}

//输出
Member类所对应的mysql表的名字是:Member
javabean中的firstName字符串属性,在mysql表中的名字是firstName,长度是30,约束条件为@wangxuan9.Constraints(allowNull=true, primaryKey=false, unique=false)
javabean中的lastName字符串属性,在mysql表中的名字是lastName,长度是50,约束条件为@wangxuan9.Constraints(allowNull=true, primaryKey=false, unique=false)
javabean中的age整型属性,在mysql表中的名字是age,约束条件为@wangxuan9.Constraints(allowNull=true, primaryKey=false, unique=false)
javabean中的handle字符串属性,在mysql表中的名字是,长度是30,约束条件为@wangxuan9.Constraints(allowNull=true, primaryKey=true, unique=false)

从上面的这个例子也可以更加明确自定义注解的作用,我们的自定义注解必须要配合使用反射机制的注解处理器一块使用。我们的自定义注解其实和注释功能差不多,都是为了加一些注释的信息,但是差别是,注释信息你是不能够在程序运行的时候手动得到的,但是我们的自定义注解想要告诉我们的注释信息,可以通过反射在运行的时候获取到。

怎样在注解里为一个嵌套注解赋默认值?如果我们想要定义一个专门接受注解的方法该怎么定义?

首先如果我们想要在一个注解里面定义一个嵌套注解,并且想给这个嵌套注解赋一个默认值,我们需要怎么定义呢?如下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Constraints{
    boolean primaryKey() default false;
    boolean allowNull() default true;
    boolean unique() default false;
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface SQLString{
    int value() default 0;
    String name() default "";
    //在@SQLString注解里面嵌套一个@Constraints注解,并且给这个注解一个默认值;对于我们的最前面的类型定义,我们就直接使用我们的注解的名字,就表示是我们的注解类型,变量名字仍需要加一个小括号。然后后面写默认值的时候,直接写@注解名字也即是@Constraints,这个就是我们@Constraints注解中的默认值。如果不写default,那么它的默认值为null
    Constraints constraints() default @Constraints;
}

如果一个方法它接收到的参数类型是@Constraints注解类型,我们需要怎么写呢?如下:

//其实我们的注解类型也是一个对象,java中一切皆对象,因此我们就可以向处理普通的类参数的时候处理注解参数,其实也就是注解的名字就表示注解的类型,和普通的class类是一样的。
public void method(Constraints constraints){
    System.out.println("是否为主键:" + constraints.primaryKey());
}

第六章 泛型

泛型出现原因

为什么会出现泛型?**在JDK5之前,我们是没有泛型的,那个时候一般的类和方法都只能使用一种具体的类型,要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制会对代码产生很大的束缚;**比如说我现在有个类如下:

class Test{
    public Student getResult(){
        ......
    }
}

像上面的这个类,对于你的getResult方法你就只能有它的一种返回值Student,但有时候我们想要让这个方法有其它类型的返回值,所以这在JDK5的时候,是不可能实现的,因为如果没有泛型,你再编译期间你的返回值类型就已经定了;但是在JDK5之后引入了泛型,在编写class类的时候不用具体指定使用的是什么类型,可以用泛型来代替。如下:

class Test<T>{
    public T getResult(){
        ......
    }
}

//然后实际我们实例化这个类型的时候,再为它指定一个具体的类型,这样我们的getResult方法就可以返回不同的类型了
new Test<Student>();
new Test<Teacher>();

泛型的第二个好处是,可以弥补不能使用多态的情况,进行解耦

比如如果现在有一个继承体系,你现在写一个方法,这个方法的参数是最上层的父类(抽象类或者接口),那么因为多态这个特点,这个方法可以接收所有的子类,所以它可以实现一个解耦,因为如果没有多态我们有多少个子类就需要定义多少个方法;但是假如我们现在没有多态继承体系,我们有很多个类,这些类没有关联,它们不是子父类的关系,而我们又想要只写一个方法,那么我们要怎么办呢?这个时候我们就可以使用泛型,如下:

class Test<T>{
    public void execute(T t){
        .....
    }
}

//在实例化这个类的时候,可以指定我们想要方法接收的参数
new Test<Student>();
//这样就可以实现同一个方法可以接收不同类型的参数的功能,在一定程度上也是一种解耦

泛型的第三个出现的理由是因为它要使用容器类

在JDK5之前java里面是没有容器类这个概念的,真正的容器是可以容纳不同的类型的,如果一个容器只能装一种类型,那么这个就不叫做容器。JDK5之前一个“容器”只能存储一种类型,如下:

class Automobile {}

//我们的Holder1容器里面只能存储一种类型的元素,那就是Automobile
public class Holder1 {
    private Automobile a;
    public Holder1(Automobile a){
        this.a = a;
    }
    Automobile get(){
        return a;
    }
}

//也有人说可以通过给容器的容纳元素定义成是Object对象,从而让此容器可以容纳不同的元素;这种说法是对,我们如果把容器内的元素类型定义成是Object类型,那么容器就可以容纳所有类型的对象;但是我们再取出容器中的元素,向下转型的时候,我们怎么可以知道它具体是什么类型呢?如果不知道是什么类型,就会发生转换异常。
public class Holder2{
    private Object a;
    public Holder2(Object a){
        this.a = a;
    }
    public void set(Object a){
        this.a = a;
    }
    public Object get(){
        return a;
    }
    public static void main(String[] args){
        Holder2 h2 = new Holder2(new Automobile());
        Automobile a = (Automobile)h2.get();
        h2.set("Not an Automobile");
        String s = (String)h2.get();
        h2.set(1);//Autoboxes to Integer
        Integer x = (Integer)h2.get();
    }
}
//上面的例子中我们可以向下转换成具体的类型,但是前提我们必须知道这个类型是什么,而我们真实开发中一般都是不会知道这个具体的类型的。

**java对容器的要求:容器必须要可以存储不同的元素类型;每次使用容器都要指定它存储一种特定类型的元素;**这听起来有点绕,第一句话的意思是容器可以存储不同的类型,比如说一个List容器里面,它既可以存储Student类型,又可以存储Teacher类型,但是每一次我们只要它存储一种类型,比如说我们在一次开发中,需要让List容器里面存储Student类型,在另外一次开发中需要让List容器里面存储Teacher类型。只要我们在编译时指定了容器所要具体存储的类型,那么编译期间,如果往容器里面存储其它的类型就会发生编译时错误。因此这可以避免由于容器中同时存在不同的元素,当我们取出来的时候发生转换异常。泛型的核心概念是告诉编译器你要使用什么类型然后编译器会帮你处理一切细节。

使用泛型改变上面没有使用泛型的容器,如下:

public class Holder2<T>{
    private T a;
    public Holder2(T a){
        this.a = a;
    }
    public void set(T a){
        this.a = a;
    }
    public T get(){
        return a;
    }
    public static void main(String[] args){
        Holder2<Automobile> h2 = new Holder2<Automobile>(new Automobile());
        Automobile a = h2.get();
    }
}

元组

什么是元组?元组就是包含多种对象类型的一种类型。为什么会出现元组?因为我们的return后面返回的之后只能跟一个返回对象,但如果我们想要返回多个对象该怎么办呢?我们可以把这多个对象写到元组里面,然后通过元组返回。

比如我们现在想要返回A,B两个对象那么我们就可以返回一个二元元组,如下:

//在元组中,我们把它的持有对象属性声明为public,但是这不会出现线程安全问题吗?不会的。因为我们的属性定义成了final的,初始化后是不允许更改的,所以当我们使用构造器初始化TwoTuple之后,属性A和属性B的值就不能修改了,就算两个线程同时操作我们的first和second对象也不会出现问题,因为初始化之后,这两个对象就不能修改了;使用泛型可以给一个元组指定持有的特定的对象类型;
public class TwoTuple<T1,T2>{
    public final T1 first;
    public final T2 second;
    public TwoTuple(T1 t1,T2 t2){
        this.first = t1;
        this.second = t2;
    }

泛型什么时候需要指定具体的实体类?

现在泛型有了,那么问题来了,一个类的泛型,我们什么时候给他指定具体的实体类型呢?什么时候需要用到实体类型,什么时候把泛型指定一个具体的实体类型。

假如我们现在有一个泛型接口,如下:

interface Shine<T>{
    void next(T t);
}

然后我们有一个类实现这个接口,如下:

//因为我们的Sun实现类,在实现接口的时候必须要实现接口的next(T t)方法,方法体里面需要操作这个T泛型,所以我们的实现类必须要知道它是什么类型,否则无法操作,因此我们的实体类需要用到我们的泛型,因此在创建Sun类的时候就需要指定接口的泛型为具体类型
class Sun implements Shine<Student>{
    void next(Studnet student){
        System.out.println(student.getId());
    }
}

//如果不指定会出现下面的情况,由于我们调用了getId()方法,但是我们不知道它是什么类型,所以会报错
class Sun implements Shine<T>{
    void next(T student){
        System.out.println(student.getId());
    }
}

如果定义class类的时候不用指定具体的泛型是什么类型,那么我们就等这个泛型第一次需要被指定的时候,比如说我们实例化这个类的时候去指定一个具体的类型,如下:

//我们定义类的时候,不需要知道这个泛型是什么,因为我们不会调用这个泛型的具体的属性或者方法,因此我们不需要知道它是什么类型;但是我们在main方法里面实例化我们的二元元组类的时候,因为必须要知道它里面持有的对象,因此我们必须要知道泛型的具体类型
class TwoTuple<T1,T2>{
    public final T1 first;
    public final T2 second;
    public TwoTuple(T1 t1,T2 t2){
        this.first = t1;
        this.second = t2;
    }
    public String toString(){
        return "(" + first + ", " + second + ")";
    }

    public static void main(String[] args) {
        TwoTuple<Integer,String> twoTuple = new TwoTuple<Integer, String>(1,"sss");
        System.out.println(twoTuple.first+" "+twoTuple.second);
    }
}

总之,就是在我们第一次需要知道泛型是什么类型的时候指定泛型类型。

单独给方法指定泛型

前面我们看到的泛型都是应用于整个类上的,但同样可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否拥有泛型方法,与其所在的类是否是泛型没有关系;

泛型方法能够使得该方法能够独立于类而产生变化,一个基本的知道原则是:无论何时,如果使用泛型方法可以取代将整个类泛型化,那么就应该尽量使用泛型方法,因为它可以使事情更加的清除明白;

并且对于我们的static方法,它并不能使用类的泛型,如下:

//因为我们的static方法不是跟随对象初始化时候生成的,它是加载class类模板的时候生成的,所以我们实例化对象的时候指定的具体类型不可以应用到static静态方法中
class Test<T>{
    public static void name(T t){
        ....
    }
}

//static方法name并不能知道它是Student类型
new Test<Student>();

//因此对于static静态方法我们只能通过给方法指定泛型来使用泛型能力
class Test{
    public static <T> void name(T t){
        ....
    }
}
//方法泛型会根据参数类型自动推断它的返回值是什么类型
Test test = new Test();
Student student = test.name(student);

例子,如下:

class GenericMethods {
    public <T> void f(T x){
        System.out.println(x.getClass().getName());
    }

    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        gm.f("");
        gm.f(1);
        gm.f(1.0);
        gm.f(1.0F);
        gm.f('c');
        gm.f(gm);
    }
}

//运行main方法的输出结果如下
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
wangxuan4.GenericMethods

让泛型方法接收多个类型不同的参数,如下:

class GenericMethods {
    public <T> void f(T x,T y,T z){
        //输出第二个参数的类型
        System.out.println(y.getClass().getName());
    }

    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        gm.f("",1,1.0F);
    }
}

//输出结果
java.lang.Integer

窄化泛型的范围

窄化泛型在类上的范围,如下:

//我们的T1泛型可以是任何的类型,但是我们的T2泛型必须是Shape类型或者是Shape类型的子类
class Holder<T1,T2 extends Shape>{
    T1 t;
    T2 t2;
    Holder(T1 t1,T2 t2){
        this.t = t1;
        this.t2 = t2;
    }
    public void test(){
        t2.test();
    }
}

//我们第一个泛型没有限制,但是第二个泛型必须要是Shape类型或者是Shape类型的子类
Holder<People, Circle> shapeCircleHolder = new Holder<People, Circle>(people, circle);

给容器加泛型

给容器加泛型之后,这个容器只能容纳指定类型的元素,如下:

//给list容器加Student泛型指定特定的类型之后,这个容器里就只能容纳Student类型的元素
List<Student> list = new ArrayList<Student>();

第七章 并发

使用Runable接口创建多线程

首先需要明白我们的Runable接口的意义,我们的Runable接口其实就是我们的任务,它并不是线程,只是我们需要把这个任务给一个线程,这样这个线程就是执行的这个任务了。而我们Runable接口里面的run()方法,其实就是指我们具体的任务代表什么。

首先创建一个继承Runbale接口的任务类,如下:

class Task implements Runnable{
    private int ticket = 10;

    public void run(){
        while(ticket>0){
            System.out.println(Thread.currentThread().getName()+"-->"+ticket--);
            //Thread.yield()是提供一种建议,让执行当前任务的线程让位给另外一个线程,但是当前线程也可以选择不让位
            Thread.yield();
        }
    }
}

任务写好之后,我们要把它分配给指定的线程,这样我们的这个线程就是执行我们的这个任务,如下:

class Test{
    public static void main(String[] args) {
        for (int i =0; i<5; i++){
            new Thread(new Task()).start();
        }
        System.out.println("我输出了");
    }
}

//我们在main线程里面创建了五个线程,然后每一个线程执行的任务都是Task类的run方法里面的任务,这样我们的就包含了六个线程,第一个是main线程,然后另外五个是我们使用new Thread循环创建的线程,然后线程里面的任务会自动并发执行

多线程并发执行的结果,如下:

我输出了
Thread-0-->10
Thread-1-->10
Thread-4-->10
Thread-0-->9
Thread-1-->9
Thread-4-->9
Thread-2-->10
Thread-4-->8
Thread-0-->8
Thread-1-->8
Thread-4-->7
Thread-2-->9
Thread-3-->10
Thread-1-->7
Thread-0-->7
Thread-1-->6
Thread-2-->8
Thread-3-->9
Thread-1-->5
Thread-0-->6
Thread-0-->5
Thread-0-->4
Thread-0-->3
Thread-0-->2
Thread-0-->1
Thread-1-->4
Thread-3-->8
Thread-2-->7
Thread-4-->6
Thread-2-->6
Thread-3-->7
Thread-1-->3
Thread-3-->6
Thread-2-->5
Thread-2-->4
Thread-2-->3
Thread-2-->2
Thread-4-->5
Thread-2-->1
Thread-3-->5
Thread-1-->2
Thread-4-->4
Thread-1-->1
Thread-3-->4
Thread-4-->3
Thread-3-->3
Thread-3-->2
Thread-4-->2
Thread-4-->1
Thread-3-->1

使用Callable接口来创建多线程

我们执行一些任务的时候执行完毕之后可能需要知道它的返回值是什么,所以这个时候就要求我们的任务必须有返回值,而我们Runnable接口的任务run()方法是没有返回值的,它不能达到这个功能;所以后面就出现了Callable接口,这个接口的任务方法是call()方法,它是有返回值的,能够达到我们的功能;不过我们实现Callable的Task任务类需要再包一层FutureTask才能把这个任务给Thread线程,这也是Callable任务和Runnable任务不同的地方,具体实现代码如下:

class Test2{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Task task = new Task();
        FutureTask<String> futureTask = new FutureTask<String>(task);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }
}

class Task implements Callable<String> {
    public String call(){
        System.out.println();
        return Thread.currentThread().getName()+"-->任务执行success";
    }
}

我们的main方法执行之后输出的结果如下:

Thread-0-->任务执行success

直接继承Thread类来创建线程

有时候我们的任务不想要使用Runnable或者是Callable来实现,我们就想要简便一点,直接把任务放到对应的线程里面,这个时候可以通过直接继承Thread线程类,然后实现任务方法run()即可。这种情况是把创建线程和创建任务放到一起了,其它的两种情况都是把创建任务和创建线程分开。代码如下:

class Test3{
    public static void main(String[] args) {
        TaskThread taskThread = new TaskThread();
        taskThread.start();
    }
}


class TaskThread extends Thread{
    @Override
    public void run() {
        System.out.println("我输出了");
    }
}

运行main方法之后的输出结果,如下:

我输出了

使用Executor线程池来执行任务

如果我们每次都创建一个Thread线程,每次都手动创建一个线程对象,并且每次任务执行完之后,又会销毁这个线程对象,这是很耗费性能的,会降低我们程序的运行效率,而且也非常不方便,所以我们后来就出现了线程池技术,我们可以创建一个包含线程的线程池,然后每次我们想要多线程运行任务的时候,只需要把我们的任务丢到池子里就可以了,这样我们的Thread线程对象的创建,销毁我们就不用管了,非常方便,最终要的时候,利用线程池我们可以避免频繁的创建,销毁线程对象,会大大提高我们的程序运行效率。

CachedThreadPool线程池

假设我们现在有A,B,C三个任务,然后我们想要把这些任务给线程池,然后线程池里面对应的线程或获取某个任务,然后执行,这样我们就可以实现我们的多线程并发执行任务了。我们可以创建一个CachedThreadPool线程池,这个线程池里面初始是没有线程的,因为你刚开始没有任务,只有你有任务之后才需要创建线程。CachedThreadPool线程池创建线程的时候是按需分配的,你需要多少个线程它就会给你创建多少个线程。比如,我们现在有一个CachedThreadPool线程池,它初始状态里面没有线程,然后我们把A任务给了这个线程池,这个线程池就会创建一个线程用来执行A任务;接着我们又把B任务给这个线程池,那么这个线程池就会再创建一个线程执行B任务;然后线程池在回收线程的时候会停止创建新的线程,比如这个时候A线程的任务处理完毕了,那么这个时候线程池就会回收A任务对应的线程,回收的过程中,CachedThreadPool线程池会停止创建线程。使用CachedThreadPool线程池执行任务的代码如下:

class Test{
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i =0; i<5; i++){
            executorService.execute(new Task());
        }
    }
}

class Task implements Runnable{
    private int ticket = 10;

    public void run(){
        while(ticket>0){
            System.out.println(Thread.currentThread().getName()+"-->"+ticket--);
            Thread.yield();
        }
    }
}

运行main方法的执行结果如下:

pool-1-thread-2-->10
pool-1-thread-1-->10
pool-1-thread-2-->9
pool-1-thread-5-->10
pool-1-thread-1-->9
pool-1-thread-5-->9
pool-1-thread-2-->8
pool-1-thread-3-->10
pool-1-thread-2-->7
pool-1-thread-2-->6
pool-1-thread-2-->5
pool-1-thread-2-->4
pool-1-thread-3-->9
pool-1-thread-3-->8
pool-1-thread-3-->7
pool-1-thread-3-->6
pool-1-thread-3-->5
pool-1-thread-5-->8
pool-1-thread-1-->8
pool-1-thread-5-->7
pool-1-thread-3-->4
pool-1-thread-2-->3
pool-1-thread-4-->10
pool-1-thread-2-->2
pool-1-thread-3-->3
pool-1-thread-5-->6
pool-1-thread-1-->7
pool-1-thread-5-->5
pool-1-thread-1-->6
pool-1-thread-5-->4
pool-1-thread-1-->5
pool-1-thread-5-->3
pool-1-thread-3-->2
pool-1-thread-2-->1
pool-1-thread-4-->9
pool-1-thread-4-->8
pool-1-thread-4-->7
pool-1-thread-4-->6
pool-1-thread-4-->5
pool-1-thread-4-->4
pool-1-thread-4-->3
pool-1-thread-4-->2
pool-1-thread-4-->1
pool-1-thread-3-->1
pool-1-thread-5-->2
pool-1-thread-1-->4
pool-1-thread-5-->1
pool-1-thread-1-->3
pool-1-thread-1-->2
pool-1-thread-1-->1
FixedThreadPool线程池

FixedThreadPool线程池与CachedThreadPool不同,CachedThreadPool是按照实际情况创建线程,而FixedThreadPool线程池是一次性创建一个固定数量的线程。好处是,这可以节省时间,因为你不用每次都为不同的任务创建一个线程了。

注意如果你有五个任务,但是你使用FixedThreadPool线程池只固定分配了四个线程,那么就会因为有一个任务没有分配给具体的线程而不被执行,如下:

class Test{
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        for (int i =0; i<5; i++){
            executorService.execute(new Task());
        }
    }
}

class Task implements Runnable{
    private int ticket = 10;

    public void run(){
        while(ticket>0){
            System.out.println(Thread.currentThread().getName()+"-->"+ticket--);
            Thread.yield();
        }
    }
}

上面的例子中,我们有5个任务,但是我们只有一个线程数为4的固定线程池,那么就会有一个任务不能被执行,上面的main方法输出的结果如下:

pool-1-thread-1-->10
pool-1-thread-3-->10
pool-1-thread-1-->9
pool-1-thread-4-->10
pool-1-thread-2-->10
pool-1-thread-4-->9
pool-1-thread-1-->8
pool-1-thread-3-->9
pool-1-thread-2-->9
pool-1-thread-4-->8
pool-1-thread-1-->7
pool-1-thread-2-->8
pool-1-thread-4-->7
pool-1-thread-2-->7
pool-1-thread-1-->6
pool-1-thread-2-->6
pool-1-thread-1-->5
pool-1-thread-2-->5
pool-1-thread-4-->6
pool-1-thread-2-->4
pool-1-thread-1-->4
pool-1-thread-2-->3
pool-1-thread-3-->8
pool-1-thread-3-->7
pool-1-thread-3-->6
pool-1-thread-3-->5
pool-1-thread-3-->4
pool-1-thread-3-->3
pool-1-thread-4-->5
pool-1-thread-2-->2
pool-1-thread-1-->3
pool-1-thread-2-->1
pool-1-thread-1-->2
pool-1-thread-4-->4
pool-1-thread-3-->2
pool-1-thread-4-->3
pool-1-thread-1-->1
pool-1-thread-2-->10
pool-1-thread-4-->2
pool-1-thread-3-->1
pool-1-thread-4-->1
pool-1-thread-2-->9
pool-1-thread-2-->8
pool-1-thread-2-->7
pool-1-thread-2-->6
pool-1-thread-2-->5
pool-1-thread-2-->4
pool-1-thread-2-->3
pool-1-thread-2-->2
pool-1-thread-2-->1
ScheduledThreadPool定时任务线程池

使用ScheduledThreadPool线程池,可以对抛入线程池里的任务定时执行。比如说每隔1s,有一个线程执行此任务,如下面的代码:

/**
 * @author xuan
 * @create 2023/7/10
 */
public class MyScheduledExecutorService {
    public static void main(String[] args) {
        // 创建任务队列;10为线程数量
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);

        TimeTask timeTask = new TimeTask();
        //执行任务
        scheduledExecutorService.scheduleWithFixedDelay(timeTask, 500, 1000, TimeUnit.MILLISECONDS);
    }
}

class TimeTask implements Runnable {
    @Override
    public void run() {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        String dateStr = sdf.format(new Date());
        System.out.println("ScheduledExecutorService执行定时任务的时间:" + dateStr);
    }
}

上述代码输出结果如下图:
在这里插入图片描述
可以发现每隔1s会有一个线程执行任务。
scheduleWithFixedDelay方法
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
它的四个参数的意思:command为被执行的线程;initialDelay为初始化后延时执行时间;period为前一次执行结束到下一次执行开始的间隔时间(间隔执行延迟时间);unit为计时单位

注意上图中的scheduleWithFixedDelay方法对应的定时任务会一直执行,但有时候我们执行执行一次定时任务该怎么办呢?该用哪个方法呢?可以使用schedule方法 如下图:
在这里插入图片描述
如果加上取消代码 那么任务不会执行 因为中途会被取消,如果不加取消方法任务会执行。代码如下:

public class ScheduleTaskExample {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        ScheduledFuture<?> scheduledFuture = scheduledExecutorService.schedule(() -> {
            System.out.println("定时任务执行!");
        }, 3, TimeUnit.SECONDS);

        scheduledFuture.cancel(true);

    }
}
SingleThreadExecutor单个线程执行多个任务

使用SingleThreadExecutor其实你可以把它当成是具有1个线程的固定线程池FixedThreadPool,但是如果是FixedThreadPool线程池它只能执行一个任务,而我们的SigleThreadExecutor线程池虽然只有一个线程,但是它却可以用这一个线程执行多个任务,只不过我们的多个任务不是并发执行的,是提交任务的顺序执行的,先提交的任务先执行,其它的任务会在后面按顺序排队,在当前任务没有执行完毕之前,后面排队的任务不会被执行,所以使用SingleThreadExecutor线程池肯定是线程安全的。代码如下:

class Test{
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i =0; i<5; i++){
            executorService.execute(new Task());
        }
    }
}

class Task implements Runnable{
    private int ticket = 10;

    public void run(){
        while(ticket>0){
            System.out.println(Thread.currentThread().getName()+"-->" + ticket--);
            Thread.yield();
        }
    }
}

输出结果如下:

pool-1-thread-1-->10
pool-1-thread-1-->9
pool-1-thread-1-->8
pool-1-thread-1-->7
pool-1-thread-1-->6
pool-1-thread-1-->5
pool-1-thread-1-->4
pool-1-thread-1-->3
pool-1-thread-1-->2
pool-1-thread-1-->1
pool-1-thread-1-->10
pool-1-thread-1-->9
pool-1-thread-1-->8
pool-1-thread-1-->7
pool-1-thread-1-->6
pool-1-thread-1-->5
pool-1-thread-1-->4
pool-1-thread-1-->3
pool-1-thread-1-->2
pool-1-thread-1-->1
pool-1-thread-1-->10
pool-1-thread-1-->9
pool-1-thread-1-->8
pool-1-thread-1-->7
pool-1-thread-1-->6
pool-1-thread-1-->5
pool-1-thread-1-->4
pool-1-thread-1-->3
pool-1-thread-1-->2
pool-1-thread-1-->1
pool-1-thread-1-->10
pool-1-thread-1-->9
pool-1-thread-1-->8
pool-1-thread-1-->7
pool-1-thread-1-->6
pool-1-thread-1-->5
pool-1-thread-1-->4
pool-1-thread-1-->3
pool-1-thread-1-->2
pool-1-thread-1-->1
pool-1-thread-1-->10
pool-1-thread-1-->9
pool-1-thread-1-->8
pool-1-thread-1-->7
pool-1-thread-1-->6
pool-1-thread-1-->5
pool-1-thread-1-->4
pool-1-thread-1-->3
pool-1-thread-1-->2
pool-1-thread-1-->1
给线程池使用shutdown方法防止新任务提交到线程池

如果给一个线程池使用了shutdown方法,那么在此shutdown方法的后面再往线程池里面添加任务的时候就会报错,如下图:

在这里插入图片描述

Callable带有返回值的任务加入到线程池里之后怎么获取任务的返回结果

把Callable带有返回值的任务加入到线程池里面,相关代码如下:

class Test{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i =0; i<5; i++){
            Future<String> taskReturn = executorService.submit(new Task());
            System.out.println(taskReturn.get());
        }
        executorService.shutdown();
    }
}

class Task implements Callable<String> {
    public String call(){
        return Thread.currentThread().getName()+"-->任务执行success";
    }
}

运行main方法之后的输出结果如下:

pool-1-thread-1-->任务执行success
pool-1-thread-2-->任务执行success
pool-1-thread-3-->任务执行success
pool-1-thread-4-->任务执行success
pool-1-thread-5-->任务执行success

中断阻塞时的任务?什么情况下可以被中断什么情况下不可以?

运行中的线程任务有的可以被中断,有的不可以被中断;比如说如果是因为调用了sleep方法而导致线程中运行的任务睡眠阻塞的,这种阻塞的任务是可以被中断的;但是如果是一个线程任务之所以阻塞是因为它正在等待获取同步锁,那么这个阻塞中的线程任务是不可以被中断的;注意一个理解的要点,打断方法是对Thread线程对象进行打断的,但是真正打断的东西,还是线程对象里面的任务,也就是打断的是任务里面的run()方法。

因为调用sleep方法而导致线程任务阻塞的情况能够被中断

public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new Thread(new SleepBlocked());
        t.start();
        t.interrupt();
    }
}

class SleepBlocked implements Runnable {
    public void run() {
        try{
            TimeUnit.SECONDS.sleep(100);
        } catch(InterruptedException e) {
            System.out.println("InterruptedException");
        }
    }
}

//运行main方法之后的输出结果
InterruptedException
    
//上面的程序代码,当执行任务的run方法的时候,调用了sleep睡眠方法,发生了阻塞。这个时候在main方法里面调用了线程的interrupt方法,可以让线程执行的正在阻塞中的任务中断,因此会抛出一个InterruptedException异常,捕获之后就会输出InterruptedException

因为等待同步锁而导致线程任务阻塞的情况不能够被中断

public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new Thread(new SynchronizedBlocked());
        t.start();
        t.interrupt();
    }
}

class SynchronizedBlocked implements Runnable {
    public synchronized void f() {
        while(true) {
            Thread.yield();
        }
    }

    public void run() {
        System.out.println("Trying to call f()");
        try {
            f();
        } catch (Exception e) {
            System.out.println("SynchronizedBlocked被中断");
        }
    }
}

//输出结果
Trying to call f()
    
//可以发现上面的输出结果并没有捕获到线程被中断的异常

上面两种情况的代码优化

像我们在实际开发的时候,会尽可能的避免去直接操作Thread线程对象,我们一般会使用Executor线程池对象来代替,这样效率比较高。如果你在Executor上调用shutdownNow(),那么它将发送一个interrupt()调用给它启动的所有线程。这么做是有意义的,因为当你完成工程中的某个部分或者整个程序时,通常会希望同时关闭某个特定的Executor的所有任务。然而,你有时候也会希望只中断某个单一任务。如果使用Executor,那么通过调用submit()而不是executor()来启动任务,就可以持有该execute()来启动任务,就可以持有该任务的上下文。submit()将返回一个泛型Future<?>,其中有一个未修饰的参数,因为你永远都不会在其上调用get(),持有这种Future的关键在于你可以在其上调用cancel(),并因此可以使用它来中断某个特定任务。如果你将true传递给cancel(),那么它就会拥有在该线程上调用interrupt()以停止这个线程的权限。因此,cancel()是一种中断Executor启动的单个线程阻塞任务的方式。使用这种方式可以改造上面的两种直接操作Thread线程对象去中断阻塞任务的情况。代码如下:

public class ThreadTest {
    private ExecutorService exec = Executors.newCachedThreadPool();
    
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        Future<?> future = exec.submit(new SleepBlocked());
        f.cancel(true);
    }
}

class SleepBlocked implements Runnable {
    public void run() {
        try{
            TimeUnit.SECONDS.sleep(100);
        } catch(InterruptedException e) {
            System.out.println("InterruptedException");
        }
    }
}

//输出
InterruptedException
    
//上面的代码执行f.cancel(true)代码之后,会打断相关线程里面的任务,所以效果和上面直接操作Thread线程对象是一样的

也并非所有等待同步锁的阻塞任务都不可以被打断,如果你使用的是ReentrantLock锁,那么它是可以被打断的

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Future<?> submit = executorService.submit(new ReentrantLockBlocked());
        Thread.sleep(1000);
        submit.cancel(true);
    }
}

class ReentrantLockBlocked implements Runnable {
    private Lock lock = new ReentrantLock();

    public ReentrantLockBlocked() {
        lock.lock();
        System.out.println("第一次可以拿到lock锁对象");
    }

    public void f()  {
        try {
            lock.lockInterruptibly();
        } catch (Exception e) {
            System.out.println("ReentrantLockBlocked被中断");
        }

    }

    public void run() {
        System.out.println("Trying to call f()");
        f();
    }
}

//输出结果
第一次可以拿到lock锁对象
Trying to call f()
ReentrantLockBlocked被中断

//上面牵涉到两个线程,一个是main线程,一个是运行ReentrantLockBlocked任务的线程。其中main线程里面刚开始就创建了ReentrantLockBlocked对象,并调用lock.lock()获取到同步锁对象,并且一直没有释放。那等到第二个线程任务再获取这个锁的时候,就会被阻塞,也就是调用lock.lockInterruptibly();这个方法的时候可以被阻塞。但是这个方法是可以被打断的,因此当获取同步锁的阻塞线程被打断后,是可以执行异常获取的内容的。

后台线程

正常情况下我们创建的线程都是非后台线程,所以在main方法执行完毕之后这些非后台线程并不会结束;但是我们可以把线程手动的设置为后台线程,这样的话当我们的main方法执行完毕之后,后台线程就会被销毁了,就不会被执行了,代码如下:

class SimpleDaemons implements Runnable{
    public void run(){
        try {
            while(true){
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread() + "" + this);
            }
        } catch (InterruptedException e){
            System.out.println("sleep() interrupted");
        }
    }

    public static void main(String[] args) throws Exception{
        for (int i = 0; i < 10; i++){
            Thread daemon = new Thread(new SimpleDaemons());
            daemon.setDaemon(true);
            daemon.start();
        }
        System.out.println("All daemons started");
        //TimeUnit.MILLISECONDS.sleep(175);
    }
}

比如上面的这段代码,如果我们把main方法后面的睡眠给去掉,那么main方法执行完毕之后,所有循环创建的daemon线程都会被销毁,这个时候我们控制台的输出结果如下:

All daemons started

你会发现只有main方法里面的输出语句被输出了;但是如果让main方法结尾的地方睡眠一下,那么这个时候因为执行main()方法的非后台线程还在执行,所以我们循环创建的后台线程就不会被销毁,这个时候的输出结果是这样的,如下:

All daemons started
Thread[Thread-2,5,main]wangxuan4.SimpleDaemons@15eda77a
Thread[Thread-5,5,main]wangxuan4.SimpleDaemons@54998dc8
Thread[Thread-4,5,main]wangxuan4.SimpleDaemons@6390768e
Thread[Thread-3,5,main]wangxuan4.SimpleDaemons@6618ceb3
Thread[Thread-9,5,main]wangxuan4.SimpleDaemons@41ad0c3c
Thread[Thread-7,5,main]wangxuan4.SimpleDaemons@2f75cfb0
Thread[Thread-6,5,main]wangxuan4.SimpleDaemons@7b988015
Thread[Thread-8,5,main]wangxuan4.SimpleDaemons@2d787f1c
Thread[Thread-1,5,main]wangxuan4.SimpleDaemons@333cafca
Thread[Thread-0,5,main]wangxuan4.SimpleDaemons@69656e74

线程捕获异常

多线程中的异常会逃逸,也就是说,如果你现在这个线程比如说执行的是一个Runnable任务,那么如果在run()方法里面出现了异常,你一定要使用try{}catch在run方法里面当场捕获处理,否则的话,一旦你throws抛出这个异常到run()方法的外部,那么你就不能使用try,catch再次捕获到此异常了,比如下面的这个例子,如下图:

在这里插入图片描述

为什么在run方法的外边再使用try,catch捕获异常的时候会捕获不到呢?因为在另外一个线程里面不能捕获到此线程的异常。

所以说,对于那些能够发现的异常,一定要在run方法里面使用try,catch及时处理,如下图:

在这里插入图片描述

但是问题是,有一些异常我们并不能直接看出来,比如说1/0这个异常,我们并不好看出来,所以在run()方法里面就不会主动的try,catch,而我们在main方法里面使用try,catch又捕获不到run()方法里面的异常,这个时候要怎么办呢?

这个时候需要我们手动的加一个未捕获异常的处理器,也即是要实现一下Thread.UncaughtExceptionHandler这个接口,这样一来,我们run方法里面所有没有捕获到的异常,都会使用我们Thread.UncaughtExceptionHandler接口的具体实现去自动捕获,代码如下:

class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
    public void uncaughtException(Thread t, Throwable e){
        System.out.println("捕获到了run方法里面没有捕获的异常: "+e);
    }
}

class ExceptionThread2 implements Runnable{
    public void run(){
        throw new RuntimeException();
    }
}

class HandlerThreadFactory implements ThreadFactory{
    public Thread newThread(Runnable r) {
        System.out.println(this + " creating new Thread");
        Thread t = new Thread(r);
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        return t;
    }
}

class TestUncaughtException{
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool(new HandlerThreadFactory());
        executorService.execute(new ExceptionThread2());
    }
}

运行结果如下:

wangxuan4.HandlerThreadFactory@4f3f5b24 creating new Thread
捕获到了run方法里面没有捕获的异常: java.lang.RuntimeException

可以发现有了我们的未捕获异常处理器,我们就不用再在run方法里面使用try,catch去手动捕获异常了,处理器会自动帮助我们捕获,我们后续就不用关心了。还有一点要说明的是,HandlerThreadFactory是我们的线程工厂,CachedThreadPool线程池里面生产线程的时候是根据工厂里面的规则生产的。

演示wait(),notify(),notifyAll()的使用场景

  • 首先wati(),notify(),notifyAll()这三个方法都是Object对象里面的方法,每一个对象里面都有这三个方法;为什么把wait(),notify(),notifyAll()这三个方法放到Object中而不直接放到Thread线程对象中呢?主要是因为,我们不仅Thread线程对象可以当做锁去使用,每一个对象都可以当成是一个锁,所以我们可以在每个对象的方法里面去使用这三个方法,使用完之后都会释放当前线程在任务方法对应的临界区中的锁;
  • 当前线程的任务方法调用wait()方法的时候,会释放当前临界区的锁,当前线程会挂起到wait()方法的这个地方。我们为什么会调用wait()方法呢?主要是因为我们当前的线程执行任务方法的某段代码的时候,因为不满足某个条件我们不能继续往下执行,但是当前线程的对应的执行任务的方法又没有可以改变这个条件的能力,所以当前线程就会通过把锁交给其他的线程来让其他的线程改变这个条件;等到其他线程改变这个条件之后,它会调用notifyAll()方法去唤醒调用wait()方法的线程称作A线程,这个时候A线程又会重新获取到临界区的锁,并且会从wait()方法的下一行代码处继续执行临界区代码;
  • wait(),notify(),notifyAll()这三个方法只能在同步代码块或者是同步代码方法中执行,这也很好理解,因为执行这三个方法的时候需要释放临界区的锁,要释放临界区的锁,那么前提也就是你必须要有临界区的锁。因此这三个方法只能在临界区里面被调用;如果在其它地方调用,虽然不会有编译时异常,但是会出现运行时异常。

演示使用这三个方法的场景,有下面这样一个例子:

我们想要把蜡涂在汽车上,然后蜡涂到汽车上之后再把蜡擦掉。我们有两个线程,一个线程负责涂蜡的任务,一个线程负责擦蜡的任务。

从上面的题目描述中可以发现这是一个多线程问题,并且按照逻辑推理可以知道,如果我们现在想要擦除蜡,那么前提汽车上必须要有蜡;如果我们想要涂蜡,那么前提汽车上必须没有蜡;因此无论是我们的涂蜡线程还是我们的擦蜡线程,它的执行都依赖于另外一个线程首先控制一些条件的更改,在其中一个A线程没有执行对应的修改条件之前,另外一个B线程只能处于wait()等待执行状态,等到A线程修改完某个条件之后,会调用notifyAll()方法去唤醒B线程,这个时候B线程才能继续往下执行。代码如下:

//首先定义一个Car汽车类,这个汽车类有一些对外的服务,可以给汽车涂蜡,擦蜡。对应的方法一共有两个,涂蜡,擦蜡
class Car{
    //汽车上是否已经被涂蜡
    private boolea waxOn = false;
    
    //往汽车上涂蜡,因为涂蜡线程和擦蜡线程是相互依赖的,所以涂完蜡之后要唤醒擦蜡线程;因为涂蜡线程和擦蜡线程是相互依赖的,假如说现在汽车上已经有蜡了,那么你就不能再次涂蜡,你必须等到擦蜡线程改变某个条件之后,也就是擦蜡线程擦除掉汽车上的蜡之后,你才能继续涂蜡;
    public synchronized void waxed(){
        if(waxOn == true){
            wait();
        }
        waxOn = true;
        notifyAll();
    }
    
    
    //擦除汽车上的蜡,因为擦蜡线程和涂蜡线程是相互依赖的,所以擦除完蜡之后要唤醒涂蜡线程;因为擦蜡线程和涂蜡线程是相互依赖的,所以如果现在汽车上没有蜡,那么你就不能擦蜡,你必须等到涂蜡线程往汽车上涂蜡之后,你才能擦蜡
    public synchronized void buffed(){
        if(waxOn == false){
            wait();
        }
        waxOn = false;
        notifyAll();
    }
}

//涂蜡任务
class WaxOn implements Runnable{
    private Car car;
    public WaxOff(Car c){
        car = c;
    }
    public void run(){
        try{
            while(!Thread.interrupted()){
                car.waxed();
            }
        }
    }
}

//擦蜡任务
class WaxOff implements Runnable{
    private Car car;
    public WaxOn(Car c){
        car = c;
    }
    public void run(){
        try{
            while(!Thread.interrupted()){
                car.buffed();
            }
        }
    }
}

public class WaxOmatic{
    public static void main(String[] args) throws Exception{
        Car car = new Car();
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new WaxOff(car));
        exec.execute(new WaxOn(car));
        exec.shutdown();
    }
}

//分析上面的WaxOmatic.java中的main方法的执行过程,假设是我们的擦蜡线程先执行的,在我们的WaxOff的run方法里面可以发现,它会执行Car的buffed()方法,但是刚开始的时候,因为我们的汽车上还没有被涂蜡,所以它的waxOn值为false,因此会进入执行wait()方法,然后擦蜡线程就会进入阻塞状态被挂起,但是此时擦蜡线程因为执行了wait()方法会释放对象锁;擦蜡线程阻塞之后我们的涂蜡线程就会执行,我们的涂蜡线程会执行Car的waxed()方法,因为waxed目前值为false,因此涂蜡线程不会执行wait()方法,它会进行涂蜡,然后把waxed设置为true,接着调用notifyAll()方法唤醒我们的擦蜡线程,我们的擦蜡线程会从Car的buffed()方法的wait()方法的后面继续执行;这就是使用wait(),notify(),notifyAll()方法来处理线程依赖的一个办法。所谓的线程依赖就是,A线程执行到临界区的某段代码的地方发现不能满足某些条件,而A线程又没有能力改变这个条件,所以它只能依赖B线程去改变这个条件,所以它需要调用wait()方法阻塞,并且会释放A线程的对象锁,因为如果不这样的话B线程就没办法进入临界区改变这个条件,那么A线程就会一直处于阻塞状态,因此调用wait()方法之后一定是要释放对象锁的。

死锁

什么时候会出现死锁?当两个线程拿着彼此需要的锁不撒手的时候,会出现死锁问题。比如说,现在有两个对象,一个是A对象一个是B对象,然后现在有两个线程,一个是线程一一个是线程二;假如线程一里面执行的任务需要先获取到A对象锁再获取到B对象锁,而线程二里面执行的任务需要先获取B对象锁再获取A对象锁,因为第一次获取锁的时候他们都能获取到,但是第二次获取锁的时候,彼此的锁都被占用了,而且谁都不撒手就会出现死锁问题了。代码如下:

//线程一执行的任务
synchronized(A){
    ......
    synchronized(B){
        ......
    }
}

//线程二执行的任务
synchronized(B){
    ......
    synchronized(A){
        ......
    }
}

//上面的线程一首先得到A对象锁,线程二首先得到B对象锁,所以二者都能进入到最外层的同步代码块;但是二者的内部代码块都进不去,因为线程一需要B锁,但是此锁被线程二占用了;而线程二需要A锁,此锁被线程一占用了。

第八章 异常

基本概念

Java的基本理念是结构不佳的代码不能运行。发现错误的理想时机是在编译阶段,然而,编译期间并不能找出所有的错误,余下的问题必须在运行期间解决。所以我们的异常就会有两种,一种是编译时异常,我们在编译期间就可以发现的异常,还有一种是运行时异常,我们必须在运行的时候才能发现的异常。

为什么要使用异常呢?

  • 使用异常可以提高我们程序的健壮性。软件开发的时候一个重要的标准是,你写出的代码必须要是健壮的,不能说在A场景下可以成功运行,但是换到B场景下之后就不能成功运行了,就会出异常了。所以我们需要使用异常机制,把程序中可能出现的异常全部处理掉,这样我们写出的程序中就没有异常了,因为所有的异常我们都有处理的方式,即便异常出现我们也能自动解决。这就能提高我们的程序的健壮性了。
  • 把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相互分离开,可以达到一个解耦的效果,比如try代码块里面就是描述的在正常执行过程中做什么事的代码,而catch代码块里就是处理问题怎么办的代码,二者是相互分离的
  • 异常可以阻止当前方法或者是try作用域继续执行下去。异常最重要的方面之一就是如果发生问题,它们将不允许程序沿着其正常的路径继续走下去。在C和C++这样的语言中,这可真是个问题,尤其是C,它没有任何办法可以强制程序在出现问题时停止在某条路径上运行下去,因此我们有可能会较长时间地忽略了问题,而代码却能一直向下执行,从而陷入了完全不恰当的状态。

异常情形与普通问题

所谓的普通问题是指,在当前环境下能够得到足够的信息,我们总能够处理这个可能发生的异常,通常普通问题都是使用try,catch进行处理的。

所谓的异常情形是指,我们在当前环境下无法获得必要的信息来解决这个问题,你只能从当前环境中跳出去,并且把问题提交给上一级环境。其实就是使用throws向上抛异常。

捕获异常:try,catch机制

要明白异常是如何被捕获的,必须首先理解监控区域的概念。它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。

try块

如果在方法内部抛出了异常(或者在方法内部调用的其他方法抛出了异常),这个方法将在抛出异常的过程中结束。比如下面的这个例子:

//main方法的中间主动向上抛出了一个异常对象,因此main方法中最后一句输出语句就不会再执行了
public class ExceptionTest {
    public static void main(String[] args) {
        String s = null;
        if (s == null)
            throw new NullPointerException();

        System.out.println("我输出了");
    }
}

//输出结果
空的什么都没有

要是不希望方法在抛出异常的时候就此结束,可以在方法内设置一个特殊的块来捕获异常。因为这个块里“尝试”各种可能会产生异常的方法的调用,因此称其为try块。它是跟在try关键字之后的普通程序块。例子如下:

//因为我们的main方法里面可能产生的异常都会被catch处理掉,所以即便前面产生了异常也无所谓,反正它会被正确的处理掉。因此我们main方法里面的最后一句是可以输出的
public class ExceptionTest {
    public static void main(String[] args) {
        try{
            String s = null;
            if(s == null)
                throw new NullPointerException();
            
            System.out.println("try代码块里发生异常之后的代码也不能够执行了");
        } catch (NullPointerException e){
            e.toString();
        }

        System.out.println("我输出了");
    }
}

//输出结果
我输出了

printStackTrace()方法

printStackTrace()方法可以打印“从方法调用处直到异常抛出处”的方法调用序列。如下图:

在这里插入图片描述

getMessage()方法

getMessage()方法可以打印异常构造器里面的字符串信息,如下图:

在这里插入图片描述

第九章 字符串

当方法的形参为String字符串,基本类型,普通对象类型的时候,方法能不能修改实际形参

String对象是不可变的。String类中每一个看起来会修改String值的方法,实际上都是创建了一个全新的Sring对象,以包含修改后的字符串内容。而最初的String对象丝毫未动

所以,String字符串虽然不是基本类型,但是我们在使用的时候不可变这个特性可以看成是基本类型。比如下面的这个例子:

class Immutable{
    public static String upCase(String s){
        return s.toUpperCase();
    }
}

public class StringTest {
    public static void main(String[] args) {
        String s = "LiBai";
        //虽然把s作为了参数传递给了方法,但是其实传递的并不是s的真正引用,而是我们根据s字符串在字符串常量池里面重新创建了一段内存空间,这段内存空间的值和我们s字符串中的值一样,然后我们把这个s引用的引用拷贝传递给Immutable.upCase()方法,所以我们Immutable.upCase()方法修改的并不是s引用内的字符串,而是修改的是s引用的引用拷贝的字符串,因此不管我们在Immutable.upCase(s)方法里面对我们的s字符串怎么修改,真正的s字符串即s = "LiBai"都是纹丝不动的。这就是String字符串的不可变特性。
        String s2 = Immutable.upCase(s);
        System.out.println(s);  //输出仍是"LiBai"
        System.out.println(s2); //输出"LIBAI"
    }
}

对于基本类型的值,传递给方法的参数之后,无论方法内部如何更改,我们的基本类型的值也不会被修改,如下:

class Immutable{
    public static int upCase(int a){
        a = 4;
        return a;
    }
}

public class StringTest {
    public static void main(String[] args) {
        int a = 8;
        //这里把基本类型数据a作为形参传递给Immutable.upCase()方法,方法内部对a进行更改,我们的a的值也不会发生修改。
        int i = Immutable.upCase(a);
        System.out.println(a);  //a的值并没有被Immutable.upCase(a)方法修改,仍然是 8
        System.out.println(i);  //输出4
    }
}

所以说,我们String类型虽然说不属于java的8种基本类型,虽然说String类型是对象类型,但是String类型的不可变特性,其实和我们的基本类型数据还是有一些相似的。

而我们的普通对象类型的数据,是可以被其它方法修改的,如下:

class Student{
    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class Test {
    public static void updateTest(Student student){
        student.setAge(18);
        student.setName("张三");
    }

    public static void main(String[] args) {
        Student student = new Student();
        student.setName("李四");
        student.setAge(14);
        //这里我们是把student对象的引用给参数作为形参了,student对象的引用就是我们对象在堆内存中存放的实际内存的地址,因此当updateTest(student)方法对我们的形参进行操作的时候,实际上就是操作的我们的堆内存中的对象的实际值。因此如果我们把一个普通对象作为参数的形参,那么我们方法里面对参数的修改,会实际影响到方法外部的对象。
        updateTest(student);
        System.out.println(student);
    }
}

//输出结果
Student{name='张三', age=18}

String.format()格式化字符串的方法

有些字符串并不是完整的字符串,它的内容里面有一些占位符,需要我们动态的手动填充这些占位符,我们可以使用String的static静态方法format来进行填充

//%s代表字符串类型的占位符,%d代表整型类型的占位符,%b代表的是布尔类型的占位符
//我们下面的这个例子中,s字符串就不是一个完整的字符串,它有一些占位符需要我们手动的填充,这里我们就可以使用format方法进行格式化填充
public class StringTest {
    public static void main(String[] args) {
        String s = "我是%s,我们有兄弟%d个,%b";
        String result = String.format(s, "葫芦娃", 7, true);
        System.out.println(result);
    }
}

//输出结果
我是葫芦娃,我们有兄弟7个,true

正则表达式

概念

一般来说,正则表达式就是以某种方式来描述字符串,因此你可以说:“如果一个字符串含有这些东西,那么它就是我正在找的东西。”比如说我们现在想找含a,b,c任意一个字符的字符串,那么我们就可以使用正则表达式[abc]去匹配我们要找的字符串。正则表达式应用的场景:比如你现在有很多个字符串,但是你不知道哪些有用哪些没有用,然后你就可以定义一个正则表达式匹配符合条件的字符串。

java中正则表达式中的反斜线\与C语言中正则表达式的反斜线\的处理是不一样的,java中和正则表达式有关的反斜线\是C语言中的double倍。在C语言中如果你想要写一个表示数字的正则表达式你需要写成是\d,如果你想要写一个普通的反斜线\也就是其后跟的字符没有特殊含义就是普通字符的反斜线,你需要写两个反斜线\;而在Java中如果你想要写一个表示数字的正则表达式你需要写成是\\d,如果你想表示一个普通的反斜线\你需要写四个反斜线\\\\;不过如果不是正则表达式,比如说java中的换行符\n和制表符\t还是写成一个反斜线就可以了。

使用正则表达式搜寻符合条件的某一个字符串

假如我现在需要通过正则表达式寻找满足条件的字符串,下面有个例子:

public class StringTest {
    public static void main(String[] args) {
        //正则表达式的规则是开头必须有0个或1个负号-,然后跟1个或多个整型,我们用这个正则表达式去筛选满足条件的字符串,因为这里的字符串开头
        //是字母,因此开头就错了,因此这句代码输出结果为false
        System.out.println("aaa-1234".matches("-?\\d+"));
        
        //开头是负号-,后面跟数字,和我们正则表达式的规则相同,因此正则表达式可以匹配到,这句代码输出true
        System.out.println("-1234".matches("-?\\d+"));
        
        //开头是负号-,后面跟数字,但是后面又跟了字母,和正则表达式的规则不相同,因此正则表达式不能匹配到,这句代码输出false
        System.out.println("-1234f".matches("-?\\d+"));
        
        //开头是数字,什么都没有了。与我们的正则表达式规则一致,可以匹配到,因此这句代码输出true
        System.out.println("5678".matches("-?\\d+"));
        
        //开头是正号+,与我们正则表达式的规则不一致,不能匹配成功,因此这句代码输出为false
        System.out.println("+911".matches("-?\\d+"));
        
        //我们这里的正则表达式变了,开头是0个或者1个负号-或者正号+,后面跟一个或多个数字。我们这里的字符串开头是一个正号+后面跟数字,
        //与我们的正则表达式规则相同,因此可以匹配成功,输出为true;因为我们java中的加号+是一个特殊符号,所以必须要使用反斜线\转义,但是
        //我们java的反斜杠规则和C语言的还不同,我们java中除了换行符\n和制表符\t,都需要用两个反斜线表示一个反斜线,因此这里把加号+这个
        //特殊符号转义成普通符号的时候,需要使用两个反斜杠进行转义
        System.out.println("+911".matches("(-|\\+)?\\d+"));
    }
}

//输出结果
false
true
false
true
false
true
    
//从上面的例子中也可以看出来,Java中的正则表达式和C语言的正则表达式的另外一个区别:对于C语言中的正则表达式,如果你不加^开头符号和$结束符号,那么我们的正则表达式可以匹配字符串中的一段内容,什么意思呢?比如说上面例子中的这句代码System.out.println("aaa-1234".matches("-?\\d+"));假如是在C语言中,因为我们的字符串中含有一段"-1234"内容,所以它就可以和我们的正则表达式匹配成功;但是在我们的java中,它默认会给我们的正则表达式的开头加上^符号,然后结尾加上$符号,我们的"-?\\d+"正则表达式相当于是"^-?\\d$",因此我们在java中"aaa-1234".matches("-?\\d+")匹配是不会成功的,因为字符串的开头是字母。
使用正则表达式分割字符串

比如我们现在想根据特殊符号分割一个字符串,如下:

public class StringTest2 {
    public static void main(String[] args) {
        String str = "abc&def#ghi`jk";
        //字符类\W表示的是非字母和数字,也即是非[a-zA-Z0-9],因此str字符串中满足我们正则表达式的分隔符有&,#,`,所以我们
        //对字符串分割的时候是从这三个特殊符号位置进行分割的,分割完成之后形成的字符串数组就是[abc,def,ghi,jk]
        String[] split = str.split("\\W");
        System.out.println(Arrays.toString(split));
    }
}

//输出结果
[abc, def, ghi, jk]
使用正则表达式替换字符串中的满足条件的部分

比如我们现在有一个字符串,这个字符串当中有一些特殊符号,我们现在想把全部的特殊符号替换成大写字母B,如下:

public class StringTest2 {
    public static void main(String[] args) {
        String str = "abc&def#ghi`jk";
        //字符类\W表示的是非字母和数字,也即是非[a-zA-Z0-9],因此str字符串中满足我们正则表达式的分隔符有&,#,`
        //所以我们替换的时候其实是替换的&,#,`这三个字符,替换完成之后得到的新的字符串就是abcBdefBghiBjk
        String result = str.replaceAll("\\W", "B");
        System.out.println(result);
    }
}

//输出结果
abcBdefBghiBjk
正则表达式中常见的字符类
正则表达式字符类含义
.任意字符
[abc]包含a,b,c的任何字符,和a|b|c作用相同
[^abc]除了a,b,c之外的任何字符 否定
[a-zA-Z]从a到z或从A到Z的任何字符 范围
[abc[hij]]任意a,b,c,h,i和j字符(与a|b|c|h|i|j作用相同)合并
[a-z&&[hij]]任意h,i或j 交集
\s空白符(空格,tab,换行,换页和回车)
\S非空白符
\d数字[0-9]
\D非数字
\w词字符[a-zA-Z0-9]
\W非词字符
逻辑操作符含义
XYY跟在X后面
X|YX或Y
边界匹配符含义
^一行的起始
$一行的边界
量词含义
X?零个或一个X
X*零个或多个X
X+一个或多个X
X{n}恰好n次X
X{n,}至少n次X
X{n,m}X至少为n次,且不超过m次
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Mr-X~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值