静态、抽象、内部类

静态、抽象、内部类

1.static

static修饰符可以修饰属性方法代码块

1.1静态属性(全局变量)

在类中,使用static修饰的属性,就是静态属性。例如,

public class Demo{
	static int num; //可共享,即使全局变量
}

注意,非静态属性,是属于对象的(实例变量),一定要使用对象来访问,没有其他方式!

静态属性,是属于类的,并且是这个类所有对象共享的,例如

静态属性在类加载的时候就存在了,即在new对象之前就存在;

可以通过类调用 Demo.num;

package com.company.static_abstro;

public class Demo {
    static int num; //静态属性,全局变量

    public static void main(String[] args) {
        Demo.num = 10;
        Demo demo1 = new Demo();
        Demo demo2 = new Demo();
        System.out.println(demo1.num);//输出结果为 10
        System.out.println(demo2.num);//输出结果为 10
        Demo.num = 20;
        System.out.println(demo1.num);//输出结果为 20
        System.out.println(demo2.num);//输出结果为 20
        demo1.num = 30;
        System.out.println(demo1.num);//输出结果为 30
        System.out.println(demo2.num);//输出结果为 30

    }
}
/*
public class Demo{
    int num; //必须要new对象才可以使用
    public static void main(String[] args){
        Demo demo=new Demo();
       // demo.num=6;
        System.out.println(demo.num);//创建对象时默认初始化=0
    }
}
*/

可以看出,无论是使用类访问静态属性,还是使用这个类的某个对象访问静态属性,效果是一样 的,这个属性对这个类的所有对象都是可见的、共享的。

静态属性的存储位置:

类中的静态属性,跟随着类,一起保存到内存的方法区,当创建对象的时候,对象中只会保存类中定义的非静态属性的信息,而静态属性是不会进入到对象中的。

静态属性的初始化:

无论是静态属性还是非静态属性,都必须初始化后才能使用,要么是系统给初始值赋默认值,要么是我们手动给属性赋值;

属性的初始化时间:

  • 非静态属性:创建对象后,系统会自动给对象中的非静态属性做默认初始化赋值,也是因为这个原因,非静态属性只有在创建对象后,使用对象采用可以访问
  • 静态属性:类加载到内存中(方法区)的时候,系统就会给类中的静态属性做**初始化默认赋值,**所以即使还没有创建对象,只要类加载到了内存中,就可以直接用类名来访文静态属性,因为这个时候静态属性已经完成了初始化默认赋值的操作;

所以,静态属性是属于类的,只要加载到了内存了,就可以使用类名来访问。非静态属性是属于对象的,必须通过创建对象,使用对象才可以访问。

内存图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mqbttxB5-1638149270654)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609153312668.png)]

  • Demo类,被加载到内存中的时候,静态属性num就已经完成了默认初始化赋值操作;
  • 可以通过类名(Demo.num)来访问,它可以直接找到方法区中存储的静态变量num;
  • 可以通过对象(demo.num)来访问,引用demo先找到堆区中的对象,再根据对象中存储的Demo.class信息,找到方法区中存储的静态变量num;
  • 通过上述可知,无论是通过类名还是对象来访问静态变量num,访问的都是同一个num,但是官方推荐用类名来访问更加合适;

注意,只有实例变量才会保存在对象中,并作初始化操作,静态变量保存在类中,并作初始化操作;

生活中的例子:

非静态属性(实例变量),就像教室中,同学们的水杯,每个同学都有一个自己的水杯,和其他人相互 不影响

静态属性,就像教室中,角落里的饮水机,它是这个教室中所有同学共享的,张三接一杯水,李四就会 看到饮水机中的水少了一些,同样的李四接一杯水,张三也会看到饮水机中的水少了一些。

public class Student{
    public static int count;
    public Student(){
    count++;
    }
}
    public static void main(String[] args){
        Student s1 = new Student();
        Student s2 = new Student();
        Student s3 = new Student();
        Student s4 = new Student();
        System.out.println(Student.count);//输出 4
}

创建对象一定会调用构造器,只要在构造器中,让变量count累加就行,因为count是一个静态变量,每个Student类型对象共享的,所以每次创建Student对象,使用到的count变量都是同一个

三种变量的对比:

全局变量:

public class Demo{
	static int num; //可共享,即使全局变量,不用创建对象就可以用,和对象没有关系,是属于类的
    //也可以通过调用对象来访问(完全没必要)
}

局部变量:

public class Demo{
   
   public  void f(){
       int num; //方法中的定义的变量
   }//在这个大括号之前该变量都可以用
	 
}

实例变量:

public class Demo{
    int num; //必须要new对象才可以使用
    public static void main(String[] args){
        Demo demo=new Demo();
       // demo.num=6;
        System.out.println(demo.num);//创建对象时默认初始化=0
    }
}

1.2静态方法

静态方法不能被重写

在类中,使用static修饰的方法,就是静态方法。例如,

public class Demo{
    public static void test(){
    }
}

静态方法的调用:

可以使用类名来调用,也可以使用对象来调用,但推荐使用名:

public class Demo{
    public static void test(){
    }
}
public static void main(String[] args){
    Demo.test();//推荐的方式
    Demo demo = new Demo();
    demo.test();//可以调用,但是不推荐
}

静态方法中不能调用类中的非静态方法或非静态属性:

public class Demo{
    public String num;
    public static void test(){
        this.num = 10;//编译报错
        this.sayHello();//编译报错
    }
    public void sayHello(){}
}

注意,静态方法中,不能访问this,所以也不能访问类中的非静态属性和非静态方法

原理:

类加载的时候(.java—>.class)加载到内存中,JVM会优先给类中的静态属性做初始化,给类中的静态方法分配内存空间;

而类中的非静态属性的初始化,非静态方法的分配空间,是要等创建对象之后才会进行;

所以类加载好之后,就可以直接使用类名访问静态属性和静态方法了;

创建好对象之后,才可以使用对象来访问非静态属性和非静态方法;

正是因为这个原因,在静态方法中,不能调用类中的非静态属性和非静态方法;

反之,在非静态方法中可是可以访问静态属性和静态方法的,因对象创建在类加载之后;

思考,是否可以在构造器中对类中静态属性做初始化?

public class Demo {
    public static int num;
    public Demo(){
        num = 10;
    }
}

这个代码编译是通过的,但是运行的时候可能会存在问题,例如

package  com.company.static_abstro;
public class Demo {
    public static int num;

    public Demo() {
        num = 10;
    }

    public static void main(String[] args) {
        System.out.println(Demo.num);//输出结果为 0
        Demo demo =new Demo();//创建对象调用了构造器
        System.out.println( demo.num);//输出结果为10
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PgPMiYWP-1638149270657)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609190030363.png)]

可以看出,虽然在构造器中给num赋值为10,但是在访问num的时候,看到的结果却是0

这是因为,构造器是创建对象是才调用的,这里我们直接用类名来访问了num属性,并没有使用到构造器,所以输出的还是默认初始化的值0;

类中的构造器,可以给非静态属性做初始化,但是不能给静态属性做初始化。因为我们可以绕过创建对 象的步骤,直接使用类名访问这个静态属性。

1.3静态代码块

静态代码块,也叫做静态初始化代码块,它的作用就是给类中静态属性做初始化的

例如

public class Demo {
    public static int num;
    static{
        num = 10;
    }
}
public static void main(String[] args){
    System.out.println(Demo.num);//输出结果为 10
}

在这个情况下,使用类名访问这个静态变量num,输出的结果就是10了。

静态代码块的执行时刻:

由于静态代码块没有名字,我们并不能主动调用,他会在类加载的时候自动执行

所以静态代码块,可以更早的给类中的静态属性,进行初始化赋值操作。

并且,静态代码块只会自动被执行一次,因为在JVM在一次运行中,对一个类只会加载一次;

匿名代码块:

和静态代码块类似的,还有一种非静态代码块,也叫做匿名代码块,他的作用是给非静态属性做初始化操作;

public class Demo {
    public int num;
    {
        num = 10;
    }
}
public static void main(String[] args){
    Demo demo = new Demo();
    System.out.println(demo.num);//输出结果为 10
}

注意,类中的构造器,既可以给非静态属性进行初始化,也可以给配合new关键字创建对象,所以匿名块使用的场景很少,他能完成的工作,使用构造器也一样可以完成;

匿名代码块执行的时刻:

由于匿名代码块没有名字,所以我们不能主动调用,他会在创建对象的时候,构造器之前,自动执行

并且每次创建对象之前,匿名代码块都会被自动执行。

例如

public class Demo {
    static {
        System.out.println("静态代码块执行");
    }
	{
    System.out.println("匿名代码块执行");
	}
    public Demo(){
        System.out.println("构造器执行");
    }
}
public static void main(String[] args){
    new Demo();
    new Demo();
}
    //输出结果为:
    静态代码块执行
    匿名代码块执行
    构造器执行
    匿名代码块执行
	构造器执行

可以看出,静态代码执行了一次,因为JVM只会加载Demo类一次,而匿名代码块会在每次创建对 象的时候,执行,然后再执行构造器。

1.4创建和初始化对象的过程:

Student s = new Student();

以这句代码为例进行说明:

  • 对Student类进行类加载,同时初始化类中静态的属性赋默认值,给静态方法分配内存空间
  • 执行父类的静态代码块(若有父类)
  • 执行类中的静态代码块
  • 堆区中分配对象的内存空间,同时初始化对象中的非静态的属性赋默认值
  • 调用Student的父类构造器
  • 对Student中的属性进行显示赋值,例如 public int age = 20;
  • 执行匿名代码块
  • 执行构造器代码
  • =号赋值操作,把对象的内存地址赋给变量s

例如

package com.company;

public class Person{
    private String name = "zs";
    public Person() {
        System.out.println("Person构造器");//2
        print();//直接去调用子类的方法
    }
    public void print(){
        System.out.println("Person print方法: name = "+name);
    }
}
class Students extends Person{
    private String name = "tom";//4
    {
        System.out.println("Student匿名代码块,此时name:"+name);//5
    }
    static{
        System.out.println("Student静态代码块");//1
    }
    public Students(){
        System.out.println("Student构造器");//6
    }
    //方法重写,故直接调用子类的方法
    public void print(){
        System.out.println("student print方法: name = "+name);//3
    }
    public static void main(String[] args) {
        Students s=new Students();//7 "=号赋值操作,把对象的内存地址赋给变量s"
        //s.print();//再次调用方法,此时在4的时候已经完成name="tom"赋值操作
    }
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PM6XYNxg-1638149270660)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609195626142.png)]

注意,

  1. 子类重写父类的方法,在创建子类对象的过程中,默认调用的一定是子类中重写后的方法
  2. 非静态属性的显式赋值,是在父类构造器执行结束之后子类中的匿名代码执行之前的时候;
  3. 以上代码中,因为方法的重写,会调用子类中重写后的print方法,同时该方法恰好实在父类构造器执行中调用的,而这个时候子类的name属性还没有赋值,所以输出结果为null;
  4. 如果此时你们代码块中也输出了name值,那么就会显示tom,因为已经完成了属性赋值;

1.5静态导入

在自己的类中,要使用另一个类中的静态属性和静态方法,那么可以进行静态导入,导入完成后, 可以直接使用这个类中的静态属性和静态方法,而不用在前面加上类名

注意,只有JDK1.5及以上版本,才可以使用静态导入

例如,没有使用静态导入的情况:

public class Demo {
    public void test(){
        System.out.println(Math.PI);//访问Math类中的静态属性PI,表示圆周率π
        System.out.println(Math.random());//访问Math类中的静态方法random(),生成随机数
    }
}

例如,使用静态导入的情况:

import static java.lang.Math.PI;
import static java.lang.Math.random;
//import static java.lang.Math.*; 全部导入
public class Demo {
    public void test(){
        System.out.println(PI);
        System.out.println(random());
    }
}

2.final

final修饰符,可以用来修饰类、变量、方法

2.1修饰类

用final修饰的类不能被继承,也就是说这个类没有子类

例如

public final class Action{
}
//编译报错
class Go extends Action{
}

思考,我们是否可以让一个类去继承String?

不能,因为String类使用final修饰的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nT3SF481-1638149270663)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609201515255.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PiWlwcqc-1638149270665)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609201656702.png)]

2.2修饰方法

用final修饰的方法可以被子类继承,但是不能被子类的重写

例如

public class Person{
    public final void print(){}
    }
//编译报错
class Student extends Person{
    public void print(){
    }
}

思考:我们是否可以在子类中重写从父类Object中继承过来的wait方法?

不能

首先看wait方法是用final修饰的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XQS66gdE-1638149270667)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609202134306.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PJeMT6FX-1638149270669)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609202223051.png)]

2.3修饰变量

用final修饰的变量就变成了常量,并且它只能被赋值一次,第二次赋值就会报错

例如,final修饰局部变量

public class Person{
    public void print(final int a){
        //编译报错,不能再次赋值,传参的时候已经赋过了
        a = 1;
    }
}

例如,

public class Person{
    public void print(){
        final int a;
        a = 1;
        //编译报错,不能再次赋值
        a = 2;
    }
}

例如,final修饰非静态成员变量

public class Person{
    private final int a;
}

对于这个final成员变量a,只有一次赋值机会。

JVM不再为其进行默认赋值,我们需要手动在以下对其进行赋值:

  • 声明的同时赋值;
  • 匿名代码块中赋值;
  • 构造器中赋值,此时还有额外要求:类中出现的所有构造器都要赋值,否则报错;

例如,final修饰静态成员变量

public static void main(String[] args) {

        final Student s = new Student();

//编译通过,可以修改s指向对象中的属性值
        s.setName("tom");
        System.out.println(s.getName());
        s.setName("zs");
        System.out.println(s.getName());

        s = new Student();
    //编译报错,不能修改引用s指向的内存地址
    }

注意,此时final指的是,引用s的指向的对象不能改变,但是可以使用s来操作当前指向的对象的属性和方法

常量

当static 和final同时修饰一个变量,此时jvm在加载的时候不会给该变量默认初始化,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gFQtnyDx-1638149270671)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210610144551842.png)]

所以必须在声明的时候初始化:private final static int count=10;

才不会报错;

但是这项显式赋值之后,因为final修饰的变量只能被赋值一次,所以称为常量;

3.abstract

abstract修饰符,可以修饰类、方法

3.1修饰方法

如果abstract修饰方法,那么该方法就是抽象方法

抽象方法的特点:

  • 只有方法声明;
  • 没有方法实现;

例如

//这就是一个普通方法,既有方法的声明,又有方法的实现
//方法的声明:public void test()
//方法的实现:{}
public void test(){}
//这就是一个只有声明没有实现的方法
public void test();
//这样的方法需要使用abstract修饰符来修饰,说明它是一个抽象方法
public abstract void test();

可以看出,声明方法的时候,加上abstract修饰符,并且去掉方法的大括号,同时结尾加上分号, 那么该方法就是一个抽象方法。

抽象方法必须定义在抽象类中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6His9kS6-1638149270673)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609205839181.png)]

3.2修饰类

如果abstract修饰类,那么该类就是抽象类。

例如

public abstract class Action{
}

抽象类和非抽象类的区别:

  • 抽象类中使用了abstract修饰符,而非抽象类没有;
  • 抽象类中可以编写抽象方法,而非抽象类中不能;
  • 抽象类中不能进行直接实例化对象的创建(但是可以使用多态),而非抽象类中可以;

抽象类和抽象方法的关系:

  • 抽象类中可以没有抽象方法
  • 抽象方法必须在抽象类中;

3.3意义

抽象类就是为了被继承

父类中的一个方法,如果被他的子类重写,并且每个子类各自的实现又不相同,那么父类中的这个方法,只有声明还有意义,而他的方法主体就变得没有任何意义;

这个时候,我们可以吧父类中的这个方法定义成抽象方法,只有方法声明,没有方法主体(也就是方法的实现);

甚至在父类中,一些方法根本就没有办法实现

例如,

public class Animal{
    public void run(){
    //这里应该编写动物奔跑的代码
    }
}

在 Animal 类中,编写 run 方法主体的时候,遇到的问题就是:

虽然我们知道动物都会有奔跑的行为,但是不同的动物奔跑的方式是完全不一样的,那么在 Animal 中,我们并不知道当前是什么动物,这个 run 方法中的代码该如何编写呢?

其实,这里的代码无论怎么编写,都是不合适的,毕竟在 Animal 中,我们并不知道要奔跑的动物到底 是哪一种,并且这个 run 方法将来一定是会被子类重写的,既然如此,那么还不如就索性把 run 定义为 抽象方法,只有方法的声明, 没有方法的实现,将来让子类去实现 run 方法好了。

例如,

public class Cat extends Animal{
    //子类重写父类中的run方法,这时候就变得非常明确了,就是一只猫走路的方式
    public void run(){
    System.out.println("我是一只猫,我踮起脚尖,优雅的走着猫步");
    }
}

当子类继承父类,父类为抽象类并且有抽象方法时(两个条件);

子类必须声明是抽象类或者调用父类的抽象方法(父类没有抽象方法即不需要);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1BHpARuw-1638149270675)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210609211445238.png)]

注意,从语法上讲,我们依然可以在Animal中对run方法进行实现,随便写点代码好了,反正又不 会被调用,因为run一定是会被子类重写的,但是这样写代码,就没有了设计的意义了。

所以,很多时候我们不能只看代码的语法是否正确,而且要关注这样的代码设计是否有意义!!

既然 Animal 类中的 run 方法被定义为了抽象方法,那么 Animal 类就一定要声明为抽象类

public abstract class Animal {
    public abstract void run();
}
class Cat extends Animal{
    public void run(){
        System.out.println("我是一只猫,我踮起脚尖,优雅的走着猫步");
    }
}

思考:既然抽象类不能被实例化创建对象,那么这个类有什么用?

抽象类是用来被子类继承的,子类继承抽象类,并且实现抽象类中的方法。

所以,当我们遇到一个抽象类的时候,第一反应,应该是这个类肯定需要被继承,然后实现里面的抽象方法,或者重写里面的普通方法。

注意,实现父类中的抽象方法和重写父类中的普通方法,只是说法不同,但是他们的语法要求,操作方法是完全一样的,可以直接把时间抽象方法当作重写普通方法;

思考:抽象类不实例化创建对象,那么抽象类中是否有构造器?

有构造器,这个构造器是让子类调用的,子类继承父类,子类创建对象的时候,会先调用父类的构造器;

思考:如果我们要编写一个类,只想让别人继承它并重写指定方法,那么我们该如何设计?

将这个类定义为抽象类即可,同时如果想要求别人继承的时候,一定要重写某个方法的话,只要把这个 方法定义为抽象方法,别人在继承这个类的时候,就一定要会对这个抽象方法进行实现!

思考:子类继承抽象父类,子类是否可以不实现父类中的抽象方法?

可以实现,子类继承抽象父类,子类可以选择实现父类中所有的抽象方法,如果有任何一个抽象方法没 有被子类实现,那么这个子类也要将自己声明为抽象类,那么这个子类也就只能等待,再来一个子类继承自己,去实现剩余没有实现的抽象方法,直到所有抽象方法都被实现为止。

3.4案例

package com.company.static_abstro;

public abstract class Animal{
    public abstract void run();
}
class Cat extends Animal{
    //子类重写父类中的run方法,这时候就变得非常明确了,就是一只猫走路的方式
    public void run(){
        System.out.println("我是一只猫,我踮起脚尖,优雅的走着猫步");
    }
}
class Dog extends Animal {
    //子类重写父类中的run方法,这时候就变得非常明确了,就是一只狗走路的方式
    public void run() {
        System.out.println("我是一只狗,我迈开抓子,悠悠晃晃的走着");
    }

    public static void main(String[] args) {
//编译报错,抽象类不能实例化创建对象
//Animal animal = new Animal();
//声明父类的引用,准备使用多态
        Animal animal;
//指向子类对象
        animal = new Cat();
//调用方法,子类重写,调用到重写后的方法
        animal.run();
//指向子类对象
        animal = new Dog();
//调用方法,子类重写,调用到重写后的方法
        animal.run();
    }
}

4.interface

引用数据类型:类、数组、接口

4.1概述

接口是除了类和数组之外,另外一种引用数据类型

接口和类不同,类的内部封装了成员变量、构造方法和成员方法,而接口的内部主要就是封装了方法静态常量

接口的定义和类很类似,但是接口需要使用 interface 关键字来定义

接口最终也会被编译成.class文件,但一定要明确接口并不是类,而是另外一种引用数据类型

例如,

//使用interface关键字来定义接口
public interface Action {
    //接口中的静态常量
    public static final String OPS_MODE = "auto";
    //接口中的抽象方法
    public void start();
    //接口的抽象方法
    public void stop();
}

注意1, 定义类使用关键字 class ,定义接口使用关键字 interface

注意2,接口中的属性都是公共的静态常量

  • 注意:常量的名字一般需要全大写

注意3,接口中的方法都是抽象方法

  • 注意:JDK8中,还允许在接口中编写静态方法默认方法
  • 注意:JDK9中,还允许在接口中编写私有方法

注意4,接口的中抽象方法,不需要使用 abstract 修饰符,因为接口中的方法默认就是抽象方法

在接口中,可把一些修饰符省去不写:

//使用interface关键字来定义接口
public interface Action {
    //接口中的静态常量
    String OPS_MODE = "auto";//默认有static和final修饰
    //接口中的抽象方法
    void start();
    //接口的抽象方法
    void stop();
    }

接口里面的属性,默认就是public static final修饰的

接口中的方法,默认就是public abstract修饰的

所以都可以省略不写

含有静态方法和默认方法的接口:(JDK8)

public interface Action {
	public default void run1() {
	// 执行语句
	}
    public static void run2() {
    // 执行语句
    }
}

JavaAPI中一些接口中定义的静态方法和默认方法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jRNaDtgn-1638149270679)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210610173030293.png)]

可以看出,绿色圆点表示public修饰的方法

上面有字母D的,就是默认方法,有字母S的就是静态方法,有个字母A的就是抽象方法;

含有私有方法的接口:(JDK9)

public interface Action {
    private void run() {
    // 执行语句
    }
}

注意,我们一般使用的接口,大多数都只是含有抽象方法

4.2接口的实现

类和类之间的关系是继承,类和接口之间的关系是实现:一个类只能有一个父类,但是可以有多个接口;

接口和类不同,类可以实例化创建对象,而接口不行,接口只能让其他类来实现他;

一个类实现了一个接口,那么这个类可以说是接口的实现类。类实现一个接口和类继承一个父类的效果类似,格式相仿,只是关键字不同,实现用implement关键字,继承用extends关键字;

一个类实现了一个接口,那么就要实现接口中所有的抽象方法,否则这个类自己就必须声明为抽象类;

例如,实现一个接口,

public interface Action {
    void run();
    void sayHello();
}
//类实现接口,没有实现接口中的抽象方法,那么这个类就必须声明为抽象类
abstract class Student implements Action{
}

不然会报错

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jPq8atUT-1638149270680)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210610184828803.png)]

public interface Action {
    void run();
    void sayHello();
}
//类实现接口,没有实现接口中的抽象方法,那么这个类就必须声明为抽象类
 class Student1 implements Action{
    @Override
    public void run(){
        
    }

    @Override
    public void sayHello() {
        
    }
}

实现全部的抽象方法就可以不定义该类为抽象类

实现多个接口:
public interface Action {
    void run();
    void sayHello();
}
 interface Mark{
    void start();
}
//类实现接口,没有实现接口中的抽象方法,那么这个类就必须声明为抽象类
 class Student1 implements Action,Mark{
    @Override
    public void run(){

    }

    @Override
    public void sayHello() {

    }

    @Override
    public void start() {
        
    }
}

一个类实现了多个接口,那么就需要吧这多个接口中的抽象方法全部都实现

JavaAPI中的String类,也实现了多个接口;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2XkNcky5-1638149270682)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210610185651390.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GPvcFN9z-1638149270683)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210610185701389.png)]

可以看出String类继承Object类的同时,又实现了三个接口;

4.3接口继承

java中,类和类直接按的继承是单继承,也就是一个子类只能有一个父类,接口和接口之间是多继承。

例如,

//实现该接口的类,将具有run功能
public interface Runnable {
    void run();
}
//实现该功能的类将有fly的功能
interface Flyable{
    void fly();
}
//实现该接口的类,将具有run的功能,fly的功能,以及Action接口独有的doSomething功能
interface Actions extends Runnable,Flyable{
    void doSomething();
}
//实现类,实现Action接口,就必须要实现Action及其父接口中的所有抽象方法
class Demo1 implements Actions{
    @Override
    public void run() {
    }
    @Override
    public void fly() {
    }
    @Override
    public void doSomething() {
    }
}

4.4多态

多态的前提是继承,必须有子父类关系才行,而类和接口之间的实现关系,其实也是继承的一种形式,所以在类和接口的实现关系中,也可以使用多态

接口的引用指向它的实现类对象:

public interface Action {
    void run();
}
class Student1 implements Action{
    @Override
    public void run() {
    }
}
class Teacher implements Action{
    @Override
    public void run() {
    }
}
class StudentTest{
    public static void main(String[] args) {
//声明接口的引用
        Action a;
//可以指向它任意一个实现类对象
        a = new Student1();
        a = new Teacher();
    }
}

接口的引用调用方法,调用到的是实现类中重写的方法

class StudentTest{
    public static void main(String[] args) {
        Action a = new Student1();
//调用到的方法,是Student中重写(实现)的方法
        a.run();
    }
}

注意,抽象方法的实现,也是方法重写的一种,形式、语法完全一致;

4.5案例

public interface Action {
    void run();
}
interface Mark{
    void star();
}
class Student1 implements Action,Mark {
    @Override
    public void run() {
        System.out.println("student run...");
    }

    @Override
    public void star() {
        System.out.println("student star...");
    }
}
class StudentTest {
        public static void main(String[] args) {
            Action a = new Student1();
            a.run();
            a.star();//编译报错
        }

    }

报错内容:star方法在Action类中不存在

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4jmjlP9I-1638149270686)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210610191413825.png)]

mian方法中, a.run() 编译运行都是正确的,但是 a.star() 方法的调用是编译错误的。

因为,在编译的时候,编译器会先检查引用 a 所属的类型( Action )中,是否存在当前要调用的方法 ( star ),如果没有那么就直接编译报错。

正确调用star()方法的代码:

class StudentTest {
        public static void main(String[] args) {
            Action a = new Student1();
            a.run();
            //  Mark mark=new Student1();//新创建一个对象
            Mark m =(Mark)a; //强制转换
           // mark.star();
            m.star();
        }
}

在这里,可以先做类型强制转换,把Action类型的引用a,转为Mark类型引用m,然后使用引用m,就 可以调用到Mark接口中的star方法,同时Student1中对star进行了实现(重写),那么最后调用的是 Student1中的star方法。

思考,上面代码中,Action类型的引用a,为什么可以转换为Mark类型的引用m?注意,Action和Mark之 间并没有任何关系

Action a = new Student();
Mark m = (Mark)a;

引用a能否转为Mark型,主要是看引用a所指的对象,是否实现了接口Mark,如果是的话,那么就可以转换成功。

同时也可以是instanceof关键字,对引用a所指向的对象进行判断,是否属于Mark类型,如果是说明该对象实现了Mark接口,属于Mark类型,

例如,

public static void main(String[] args) {

        Action a = new Student1();
//返回true,则表示a指向的对象,也同时实现了Mark接口,属于Mark类型,那么就可以做强制转换
        if(a instanceof Mark){
            System.out.println(a instanceof Mark);//输出true
            Mark m = (Mark)a;
        }
    }

思考,如何让开发人员,一定能按照我们的预先设计好的方法,去编写代码?

用final修饰符修饰方法,即方法不能被访问;

小结:

意义:为了被实现;

implement关键字,就是实现接口==特殊继承;

接口中定义的属性

  • 必须初始化或者在匿名块中赋值;

  • 默认有static和final修饰的特性;(是常量)

接口中没有构造器

接口中只能定义抽象方法(不能被具体实现);

接口做到多实现(类似与多继承);

5.访问控制

对象中的属性和方法,是可以根据指定修饰符来进行访问控制的。具体的控制就是,这些属性和方法可以在说明地方被访问,以及在什么地方不能被访问;

5.1概述

类中的属性和方法,可以使用以下四种修饰符进行访问控制:

public >protected >default>private

  • public是公共的,在所有地方都可以访问;

  • protected,受保护的,当前类中,子类中,同一个包中的其他类都可以访问;

  • default,默认的,当前类中,同一个包中的其他类类中可以访问;

    • 注意,default默认的,指的是空修饰符,并不是default关键字;
    • 例如,String name;在类中,这种情况就是默认的修饰符;
  • private,私有的,只有在当前类中才可以访问;

修饰符同类中同一个包中(子类与非子类类)不同包中的子类不同包的非子类
publicYYYY
protectedYYYN
defaultYYNN
privateYNNN

5.2案例

package com.company.visit.pkt1;

public class SameCla {
    public String public_str = "public_str";
    protected String protected_str = "protected_str";
    String default_str = "default_str";
    private String private_str = "private_str";
    //在当前类中访问
    public void test(){
        System.out.println(public_str);
        System.out.println(protected_str);
        System.out.println(default_str);
        System.out.println(private_str);
    }

}
package com.company.visit.pkt1;

import com.company.Day8.Salesman;

public class subclass extends SameCla{


    //在同包的子类中访问
    public void test(){
//继承后,可直接访问
        System.out.println(public_str);
        System.out.println(protected_str);
        System.out.println(default_str);
//编译报错,无法访问
//        System.out.println(private_str);
    }

}
package com.company.visit.pkt1;

public class other {
    //在同包的其他类中访问
    public void test(){
        //需要先创建对象,然后再访问
        SameCla ac = new SameCla();
        System.out.println(ac.public_str);
        System.out.println(ac.protected_str);
        System.out.println(ac.default_str);
//编译报错,无法访问
        System.out.println(ac.private_str);
    }
}
package com.company.visit.pkt2;

import com.company.visit.pkt1.SameCla;

public class sub2 extends SameCla {
    //在不同包的子类中访问
    public void test(){
//继承后,可直接访问
        System.out.println(public_str);
        System.out.println(protected_str);
        //编译报错,无法访问
        System.out.println(default_str);
        System.out.println(private_str);
    }
}
package com.company.visit.pkt2;

import com.company.visit.pkt1.SameCla;

public class deferent_cl {
    //在不同包的其他类中访问
    public void test(){
        //需要先创建对象,然后再访问
        SameCla ac = new SameCla();
        System.out.println(ac.public_str);
        //编译报错,无法访问
        System.out.println(ac.protected_str);
        System.out.println(ac.default_str);
        System.out.println(ac.private_str);
    }
}

思考,正常情况下,编写一个类,都可以使用哪些权限控制修饰符?

正常编写的类,可以使用俩种权限控制修饰符:public和default

例如,

public class Person{}
class Student extends Person{}

但是,如果是内部类的话,则可以使用四种权限控制修饰符:

例如,

public class Test{
    private class A{}
    class B{}
    protected class C{}
    public class D{}
}

Test类的内部,嵌套类四个内部类:A B C D,分别使用了private 、default、protected、public进行了修饰

6.内部类

内部类,不是在一个java源文件中编写两个平行的类

6.1 成员内部类

在类中,可以定义成员方法、成员变量,除此之外,还可以定义成员内部类

成员内部类内部不能,不能定义静态的内容

例如

//外部类
public class MemberOuterClass{
    //外部类的属性
    private String name;
    private static int age;
    //外部类的方法
    public void run(){}
    public static void go(){}
    /* 成员内部类 声明开始 */
    public class MemberInnerClass{
        private String name;
        private int age;
        public void run(String name){}
        }
    /* 成员内部类 声明结束 */
}

注意,成员内部类中,不能编写静态的属性和方法;

注意,当前代码编译成功后。会生成两个class文件,一个对应外部类,一个对应内部类编译生成两个class文件的名字分别为:

MemberOuterClass.class MemberOuterClass$MemberInnerClass.class

成员内部类和外部类的相互访问:
  1. 成员内部类访问外部的属性和方法
public class MemberOuterClass {
    //外部类的属性
    private String name;
    private static int age;
    //外部类的方法
    public void run(){}
    public static void go(){}
    /* 成员内部类 声明开始 */
    public class MemberInnerClass{
        private String name;
        private int age;
        public void run(String name){
        //访问当前run方法中的参数name
        System.out.println(name);
        //访问内部类自己的属性name
        System.out.println(this.name);
        //访问外部类的非静态属性        		
        System.out.println(MemberOuterClass.this.name);
        //访问外部类的静态属性
        System.out.println(MemberOuterClass.age);
        //访问外部类的非静态方法
        MemberOuterClass.this.run();
        //访问外部类的静态方法
        MemberOuterClass.go();
        }
    }
    /* 成员内部类 声明结束 */
}

2.外部类访问成员内部类的属性和方法:

public class MemberOuterClass {
    //外部类的方法,访问成员内部类的属性和方法
    public void test(){
        //需要创建内部类对象,然后才可以访问
        MemberInnerClass t = new MemberInnerClass();
        System.out.println(t.name);
        System.out.println(t.age);
        t.run("tom");
    }
    /* 成员内部类 声明开始 */
    public class MemberInnerClass{
        private String name;
        private int age;
        public void run(String name){
        }
    }
    /* 成员内部类 声明结束 */
}

在其他类中使用这个内部类:

如果这个成员内部类不是private修饰的,那么在其他类中就可以访问到这个内部类:

package com.briup.day09.demo;
import com.briup.day09.demo.MemberOuterClass.MemberInnerClass;
public class Test {
    public static void main(String[] args) {
        MemberOuterClass moc = new MemberOuterClass();
        MemberInnerClass mic = moc.new MemberInnerClass();
        mic.run("tom");
    }
}

在其它类中,使用这个非private修饰的成员内部类的时候要注意以下几点:

  1. 这个内部类需要import导入,并且是外部类.内部类的形式导入;
  2. 在创建对象的时候,需要先创建外部类的对象,然后使用外部类对象再创建内部类对象,形式为:外部类对象.new 内部类对象

javaAPI中使用的成员内部类:

java.util.ArrayList类中,就定义了好几个成员内部类,并且还是private修饰的[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8JxkvjCg-1638149270688)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210614224009018.png)]

思考,类的内部除了嵌套另一个类之外,是否还可以嵌套接口?

可以,不仅类中可以嵌套接口,接口的内部也可以嵌套其他接口。

例如,参考java.util.Map接口中的内部接口Entry

思考,什么情况下会使用内部类?

在对事物进行抽象的时候,若一个事物内部还包含其他事物,就可以考虑使用内部类这种结构。

例如,汽车(Car)中包含发动机(Engine) ,这时, Engine 类就可以考虑(非必须)使用内部类来描 述,定义在Car类中的成员位置。

这样设计,既可以表示Car和Engine的紧密联系的程度,也可以在Engine类中很方便的使用到Car里面的属 性和方法

例如,

public class Car { //外部类
    class Engine { //内部类
    }
}

例如,人(Person)中包含心脏(Heart),这时,Heart类可以考虑(非必须)使用内部类来描述,定 义在Person类中的成员位置。

例如,

public class Person { //外部类
    class Heart { //内部类
    }
}

注意,这是从程序中,类和类之间的关系和意义进行考虑而设计的,其实这里即使不使用内部类的结构,使用普通的俩个类也能完成功能,但是内部类的结构设计会加符合实际意义,也能够好的完 成功能,因为内部类访问外部的属性和方法会更加容易。

内部类中使用外部类的实例属性:外部类this.外部类属性名

6.2静态内部类

静态内部类和成员内部类使类似的,只是这个内部类

静态内部类和成员内部类使类似的,只是这个内部类多了个static关键字进行修饰;

例如

 //外部类public class StaticOuterClass{    /* 静态内部类 声明开始 */    public static class StaticInnerClass{    }    /* 静态内部类 声明结束 */}	

注意,静态内部类,可以编写静态的属性和方法,另外在四种内部类中,只有静态内部类可以编写静态属性和方法

编译生成的俩个class文件的名字分别为:

​ StaticOuterClass.class

​ StaticOuterClass$StaticInnerClass.class

静态内部类和外部类的相互访问:

1.静态内部类访问外部的属性和方法

public class StaticOuterClass {
    //外部类的属性
    private String name;
    private static int age;
    //外部类的方法
    public void run(){}
    public static void go(){}
    /* 静态内部类 声明开始 */
    public static class StaticInnerClass{
        private String name;
        private static int age;
        public void run(String name){
        //访问当前run方法中的参数name
        System.out.println(name);
        //访问内部类自己的属性name
        System.out.println(this.name);
        //访问内部类自己的静态属性age
        System.out.println(age);
        //静态内部类中,无法访问外部类的非静态属性和方法
        //System.out.println(StaticOuterClass.this.name);
        //StaticOuterClass.this.run();
        //访问外部类的静态属性和方法
        System.out.println(StaticOuterClass.age);
        StaticOuterClass.go();
        }
    }
    /* 静态内部类 声明结束 */
}

注意,静态内部类中访问不了外部类中的非静态属性和方法;

  1. 外部类访问静态内部类的属性和方法
public class StaticOuterClass {
    public void test(){
        //外部类中,访问静态类中的静态属性
        System.out.println(StaticInnerClass.age);
        //外部类中,访问静态内部类中的非静态属性和方法
        StaticInnerClass sic = new StaticInnerClass();
        System.out.println(sic.name);
        sic.run("tom");
    }
  /* 静态内部类 声明开始 */
    public static class StaticInnerClass{
        private String name;
        private static int age;
        public void run(String name){
        }
    }
    /* 静态内部类 声明结束 */
}  

在其他类中使用这个内部类:

如果这个静态内部类不是private修饰的,那么在其他类中就可以访问到这个内部

import com.briup.sync.StaticOuterClass.StaticInnerClass;
public class Test {
    public static void main(String[] args) {
        StaticInnerClass sic = new StaticInnerClass();
        sic.run("tom");
    }
}

在其他类中,使用这个非private修饰的静态内部类,需要注意以下几点:

  1. 这个内部类需要import导入,并且外部类.内部类的形式导入;
  2. 在创建对象的时候,直接使用这个静态内部类的名字即可:new 静态内部类对象();不再需要依赖外部类对象了

javaAPI中的静态内部类:

java.lang.Integer类中,就定义一个静态内部类,并且还是private修饰的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EEIjMxAN-1638149270690)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210615095425381.png)]

思考,观察Integer类中私有静态内部类IntegerCache,它的作用是什么?

Integer num1 = Integer.valueOf(127);
Integer num2 = Integer.valueOf(127);
System.out.println(num1 == num2); //输出结果为 true

Integer num3 = Integer.valueOf(128);
Integer num4 = Integer.valueOf(128);
System.out.println(num3 == num4);//输出结果为 false

注意,==表示要比较这两个对象的内存地址值是否相等;

6.3局部内部类:(很少用)

局部内部类,是另一种形式的内部,在声明在外部类的方法中,相当于方法中的局部变量的位置,他的作用范围只是在当前方法中。

局部内部类使最不常用的一种内部类。

例如,

public class LocalOuterClass{
    public void sayHello(String name){
        /* 局部内部类 声明开始 */
        class LocalInnerClass{
        }
        /* 局部内部类 声明结束 */
    }
}

局部内部类和外部类的相互访问:

  1. 局部内部类访问外部的属性和方法
public class LocalOuterClass {
    //外部类的属性
    private String name;
    private static int age;
    //外部类的方法
    public void run(){}
    public static void go(){}
    
    public void sayHello(String name){
   		 /* 局部内部类 声明开始 */
    	class LocalInnerClass{
    		private String name;
    		public void test(String name){
   				 //访问当前test方法中的参数name
    			System.out.println(name);
                //访问内部类自己的属性name
                System.out.println(this.name);
                /*注意,sayHello方法的参数name,无法访问,因为实在没有办法表示了,换成其他
                名字后,就可以访问了,不要叫name就行*/
                //访问外部类的非静态属性
                System.out.println(LocalOuterClass.this.name);
                //访问外部类的非静态方法
                LocalOuterClass.this.run();
                //访问外部类的静态属性和方法
                System.out.println(LocalOuterClass.age);
                LocalOuterClass.go();
                }
    	}
    /* 局部内部类 声明结束 */
    }
}
  1. 局部内部类中,访问当前方法中的变量,这个变量必须是final修饰的
public void sayHello(final String name){
    final int num = 1;
    /* 局部内部类 声明开始 */
    class LocalInnerClass{
    public void test(){
        System.out.println(name);
        System.out.println(num);
        //编译报错,final修饰的变量,只能赋值一次
        //name = "tom";
        //num = 2;
        }
    }
    /* 局部内部类 声明结束 */
}

在jdk1.8中,一个局部变量在据部内部类进行访问了,那么这个局部变量自动变成final修饰

  1. 外部类访问局部内部类的属性和方法
public void sayHello(String name){
    /* 局部内部类 声明开始 */
    class LocalInnerClass{
        private int num;
        public void test(){
        }
    }
    /* 局部内部类 声明结束 */
    //创建局部内部类对象
    LocalInnerClass lic = new LocalInnerClass();
    //对象访问属性
    System.out.println(lic.num);
    //对象调用方法
    lic.test();
}

局部内部类,只能在当前声明的方法中进行使用。

6.4匿名内部类:(重要)

对于接口就相当于实现接口;

对于类就相当于继承类;

匿名内部类中一般都是会重写接口中的方法或者重写’父类’中的方法;

匿名内部类,是一种没有名字的内部类,他是内部类的一种简化写法。在之后的代码中,匿名内部类是使用最多的一中内部类。

在普通的代码中,使用一个接口的步骤如下:

  • 声明一个类,,去实现这个接口;
  • 实现这个接口中的抽象方法(重写);
  • 在其他代码中,创建这个类的对象;
  • 调用这个类中实现(重写)后的方法;

其实,在这个过程中,我们的目的就是吧接口中的抽象方法给实现,最后调用这个已经被实现的方法;

那么,匿名内部类就是把这个过程给简化了,让我们更加方便的调用到重写后的方法

格式:

父类或者接口类型 变量名 = new 父类或者接口(){
// 方法重写
@Override
public void method() {
    // 执行语句
    }
};
//调用实现(重写)后的方法
变量名.method();
匿名内部类的两种形式:
  • 利用一个父类,进行声明并创建匿名内部类对象,这个匿名内部类默认就是这个父类的子类型;
  • 利用一个接口,进行声明并创建匿名内部类对象,这个匿名内部类默认就是这个接口的实现类;
匿名内部类因为没有类名:
  • 匿名内部类必须依附于一个父类型或者接口
  • 匿名内部类在声明的同时,就必须创建对象,否则后面就没法创建;
  • 匿名内部类中无法定义构造器;

例如,利用父类型来声明并创建匿名内部类对象;

public abstract class Animal {
	public abstract void run();
}
class Test{
    public static void main(String[] args) {
    Animal animal = new Animal(){
        @Override
        public void run() {
            System.out.println("匿名内部类中的默认实现");
            }
        };
    animal.run();
    }
}

注意,如果利用父类型声明这个匿名内部类,那么这个匿名内部类默认就是这个父类型的子类

例如,利用接口来创建匿名内部类对象;

public interface Action {
    void run();
    }
class Test{
    public static void main(String[] args) {
        Action a = new Action(){
        @Override
        public void run() {
            System.out.println("匿名内部类中的默认实现");
            }
        };
    a.run();
    }
}

注意,如果利用接口声明匿名内部类,那这个匿名内部类默认就是这个接口的实现类;

思考,对比之前普通的方式,匿名内部类是不是在调用到重写方法的同时,简化了之前很多步骤?

//Algorithm 算法接口
public interface Algorithm{
    void sort(int[] arr);
    }
class Test{
    //使用指定算法,对数组arr进行排序
    public void sort(int[] arr,Algorithm alg){
        alg.sort(arr);
        }
}
 public static void main(String[] args){
      Test t = new Test();
      int[] arr = {4,1,6,3,8,5,9};
        Algorithm alg = new Algorithm(){
            public void sort(int[] arr){
            //使用当前需要的排序算法
            //例如,这里简单的使用Arrays工具类中的排序方法
            java.util.Arrays.sort(arr);
            }
    };
	t.sort(arr,alg);
}

注意:匿名内部类中不能在给外部的属性重新赋值,因为对于内部类来说这里外部的属性默认有final修饰

内部类的选择:

假设现在已经确定了要使用的内部类,那么一般情况下,该如何选择?

  1. 考虑这个内部类,如果反复的进行多次使用(必须有名字)
    • 在这个内部类中,如果需要定义静态的属性和方法,选择使用静态内部类
    • 在这个内部类中,如果需要访问外部类的的非静态属性和方法,选择使用成员内部类
  2. 考虑这个内部类,如果只需要使用一次(可以没有名字)
    • 选择使用匿名内部类
  3. 局部内部类几乎不会用;

7.包装类

java中的八种基本数据类型,他们只表示一些简单的数字,这些数字最小的在内存中占8位,最大的占64位。这些都是简单的数字,不是对象,所以也不能用来调用方法或者属性。

7.1概述

针对这八种基本类型,JavaAPI又专门提供了对应的类类型,目的就是为了分别把这八种基本类型的数 据,包装成对应的类类型,这时候就变成对象了,就可以调用方法了或者访问属性了。

基本类型包装类型
bytejava.lang.Byte
shortjava.lang.Short
intjava.lang.Integer
longjava.lang.Long
floatjava.lang.Float
doublejava.lang.Double
booleanjava.lang.Boolean
charjava.lang.Charcater
7.2案例

在这些包装类型中,都定义相关的属性和方法,例如Integer中:

package com.company.static_abstro;

public class Integer_test {

    public static void main(String[] args) {
        int i=1;
        //编译报错,因为i不是对象
        //i.toString();

//        int -->Integer 包装
        Integer o = new Integer(i);
        Integer o1=Integer.valueOf(i);
        System.out.println(o);//输出1
        System.out.println(o1);//输出1

        //Integer --> int 拆箱
        int j=o.intValue();

        System.out.println(o.toString());//输出1

        System.out.println(Integer.MAX_VALUE);//范围
        System.out.println(Integer.MIN_VALUE);

        System.out.println(Integer.toString(100,2));//二进制
        System.out.println(Integer.toString(100,8));//八进制
        System.out.println(Integer.toString(100,16));//十六进制

        System.out.println(Integer.toBinaryString(100));//把100转成二进制

        //把字符串"100"转为int类型的100
        int num = Integer.parseInt("100");
        System.out.println(num);

    }
}
7.3自动装箱/拆箱

JDK1.5或以上,可以支持基本类型和包装类型之间的自动装箱、自动拆箱:

简化了基本类型和包装类型之间的转换,这里以int和Integer为例说明

//JKD1.5 之前
Integer o = new Integer(1);
Integer o = Integer.valueOf(1);

//JDK1.5 之后
//自动装箱,这里会自动把数字1包装成Integer类型的对象
Integer o = 1;

//JKD1.5 之前
Integer o = new Integer(1);
int i = o.intValue();

//JDK1.5 之后
Integer o = new Integer(1);
//自动拆箱,这里会自动把对象o拆箱为一个int类型的数字,并把数字赋值给int类型的变量i
int i = o;

注意,其他的基本类型和包装类型之间的转换,与此类似

注意事项:

public void test1(int i){}
    public void test2(Integer i){}
    public void test3(long i){}
    public void test4(Long i){}

    public static void main(String[] args) {
        Integer_test t =new Integer_test();
        t.test1(1);//编译通过,int i=1;正常赋值
        t.test2(1);//编译通过,Integer i = 1; 自动装箱
        t.test3(1);//编译通过, long i = 1; 隐式类型转换
        //编译报错
    //错误的代码:Long i = 1;
    //int和Long 之间没有任何关系

        t.test4(1);
        t.test4(1L);//编译通过 Long i = 1L; 自动装箱
    }

8 .Object中常用方法

Object类是所有类的父类型,类中定义的方法,java中所有对象都可以调用

8.1 toString方法

该方法可以返回一个对象默认的字符串形式:

public class Object{
    public String toString() {
    	return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
}

子类可以对该方法重写:

public class Student{
    private String name;
    private int age;
    //get
    //set
    public String toString() {
    	return "Student[name="+name+",age="+age+"]";
    }
}

toString方法的调用:

public static void main(String[] args){
    Student stu = new Student();
    //这个俩个输出语句的效果是一样的
    System.out.println(stu);
    System.out.println(stu.toString());
}

注意,默认情况下。println方法会调用这个对象的toString方法;

注意,推荐使用第一种,因为当stu的值为null时,第二种输出方式会报错,空指针异常

8.2 getClass方法

这是一个非常重要的方法,它可以返回一个引用在运行是所指向的对象。具体类型是什么

该方法时native修饰的本地方法,不是java语言实现的。

public class Object{
	public final native Class<?> getClass();
}

子类中不能重写getClass方法,调用的一定是Object的中getClass方法:

public void test(Object obj){
    //obj可以接收任意类型的对象
    //getClass方法可以返回obj指向的对象具体是什么类型的
    //也就是该对象到底是使用哪个类创建出的对象
    System.out.println(obj.getClass());
}

8.3 equals方法

该方法可以比较两个对象是否相等

public class Object{
    public boolean equals(Object obj) {
   	 return (this == obj);
    }
}

可以看出来,Object中的equals方法,是直接使用的==号进行的比较,比较两个对象的地址值是否相等

例如,

public static void main(String[] args){
    Student o1 = new Student();
    Student o2 = new Student();
    boolean f = o1.equals(o2);
    System.out.println(f);
}

子类中可以对该方法进行重写:

public class Student {
    private String name;
    private int age;
    public Student(){}
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public boolean equals(Object obj) {
        //如果obj为null,直接返回false
        if(obj == null){
        	return false;
    	}
        //如果obj和this的地址值相等,直接返回true
        if(this == obj){
        	return true;
        }
		//如果obj不属于Student类型的对象,直接返回false
        if(!(obj instanceof Student)){
      	  return false;
        }
        //obj属于Student类型的对象,做类型强制转换
        Student other = (Student) obj;
        //如果obj的name和age分别等于this的name和age,直接返回true
        if(this.name.equals(other.name) && this.age==other.age){
       	 	return true;
        }
	//其他情况,直接返回false
	return false;
	}
}
class StudentTest{
    public static void main(String[] args) {
        Student s1 = new Student("tom",20);
        Student s2 = new Student("tom",20);
        System.out.println(s1 == s2);//输出false,因为俩对象的内存地址不同
        System.out.println(s1.equals(s2));//输出true,因为重写了equals的比较规则,name
        和age相等就算俩对象相等
    }
}

按照重写后的equals比较规则,只要俩个对象的name和age属性值都相等,那么就返回true,说明 俩对象相等

前面的八大包装类型都对equals方法进行了重写

对equals方法的重写,一般需要注意以下几点:

  1. 自反性:对于任意引用obj,obj.equals(obj)的返回值一定为true;
  2. 对称性:对于任意引用o1、o2,当且仅当o1.equals(o2)返回值为true时,o2.equals(o1)的返回值一定为true;
  3. 传递性:如果o1.equals(o2)为true, o2.equals(o3)为true,则o1.equals(o3)也一定为true
  4. 一致性:如果参与比较的对象没任何改变,则对象比较的结果也不应该有任何改变
  5. 非空性:任何非空的引用obj,obj.equals(null)的返回值一定为false
8.4 hashCode方法

该方法返回一个int值,该int值是JVM根据对象在内存中的特征(地址值),通过hash算法算出一个结果;

Hash,一般翻译叫做“散列”,也可以音译为:哈希。就是把任意长度的数据输入,通过散列算法,变换成固定长度的输出,该输出就是散列值;

一个任意长度的输入转为一个固定长度的输出,是一种压缩映射,也就是说,散列值的课间通常远远小于输入空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值

所以,我们可以认为,Object中的hashCode方法默认返回的是对象的内存地址,但实际上可能并不是;

public class Object{
	public native int hashCode();
}

对于两个对象的hashCode值:

  • 相等的两个对象,hashCode值一定相等;
  • hashCode值相同,两个对象有可能相同,也有可能不同;
  • hashCode值不同,两个对象不一定不同;

对象和它的hashCode值的关系,就相当于人和他们姓氏的关系:

  1. 相等的两个人,姓氏肯定是一致的;
  2. 姓氏相同的两个人,不一定是同一个人;
  3. 姓氏不同的两个人,一定是不同的两个人;

思考,hashCode在代码中有什么用?

现在有100个对象,突然又来了一个对象obj,那么怎么判断这个obj对象和之前的100个对象是否有相等 的呢?

除了使用obj对象的equals方法和之前的100个对象比较100次之外,是否还有更好的方式?

9 .关于String对象

字符串String,是程序中使用的最多的一种数据,JVM在内存中专门设置了一块区域(字符串常量池),来提高字符串对象的使用率;

9.1 概述

创建字符串对象,和其他普通对象一样,会占用计算机的资源(时间和空间),作为最常用的数据类型,大量频繁的创建字符串对象,会极大程度的影响程序的性能。

JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化

  • 为字符串开辟了一个字符串常量池,类似于缓冲区;
  • 创建字符串常量时,首先会检查字符串常量池中是否存在该字符串,如果存在,则返回该实例的引用,如果不存在,就实例化创建该字符串,并放入池中;

String s=new String("abc");

上述语句创建了两个对象…其内容都是"abc".注意,s 不是对象,只是引用.只有 new 生成的才是对象.

,首先括号里的"abc"先到 String pool 里看有没"abc"这个对象,没有
则在 pool 里创建这个对象…所以这里就在 pool 创建了一个"abc"对象.然后 通过
new 语句又创建了一个"abc"对象…而这个对象是放在内存的堆里. .这里的 s 指
向堆里的对象.

String s1 = "abc";

,s1 当然还是引用 .后面的"abc".其实就是上面括
号里的"abc".执行的是相同的操作.即 在 pool 里查找有没"abc"这个对象.没有则
创建一个…很显然,第一条语句在 pool 里已经创建了一个"abc".所以这条语句没
有创建对象,s1 指向的是 pool 中的"abc"

String s = new String("abc");
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s == s1.intern());//false
System.out.println(s == s2.intern());//false
System.out.println(s1 == s2.intern());//true
System.out.println(s1.intern() == s2.intern());//true

s1.intern().他的执行流程是,在 pool 里去查找 s1 对应的内容(也就是"abc").如果
找到,则返回 pool 里的对象.如果没有(老实说,我没想到有哪种情况是没有的),则
在 Pool 创建这个对象,并返回…
这样就很容易理解了.s1.intern 返回的是 pool 里的"abc"对象.与 s 这个堆里的对
象肯定不同,返回 false.同理,s 与 s2.intern()也肯定不同,返回 false.第三个,s1 与
s2.intern().其中 s2.intern()返回的是 pool 中的"abc"对象,而 s1 也是指向 pool 中
的"abc"对象.所以返回的是 true:

9.2 案例

案例1:

 public static void main(String[] args) {
        //只有用双引号,里面加入文字的方式,才会利用到内存中的字符串常量池。
        String str1 = "hello";
        String str2 = new String("hello");
//String中对equals进行了重写,比较的是字符串中每一个字符是否相等
        System.out.println(str1.equals(str2));//true
//str1指向的是常量池中的hello对象
//str2指向的是堆区中新创建的hello对象
        System.out.println(str1 == str2);//false
    }

只有用双引号,里面加入文字的方式,才会用到内存中的字符串常量池

对应的内存图为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FFhPikm9-1638149270691)(C:\Users\ZYZ\AppData\Roaming\Typora\typora-user-images\image-20210615172427638.png)]

案例2:

String str1 = "hello";
        String str2 = "hello";
//String中对equals进行了重写,比较的是字符串中每一个字符是否相等
        System.out.println(str1.equals(str2));//true
//str1指向的是常量池中的hello对象
//str2指向的是常量池中的hello对象
//str1和str2指向的是同一个对象
        System.out.println(str1 == str2);//true

案例3:

		String s1 = "a";
        String s2="b";

        //使用+拼接的字符串也会利用字符串常量池
		//但是有要求:参与+号拼接的必须是双引号的形式才可以。
        String s3 = "a"+"b";
        String s4 = s1+s2;
        System.out.println(s3.equals(s4));//true
		//s3指向的是常量池中的"ab"对象
        //s4指向的堆区中新建的"ab"对象
        //因为s4是由s1和s2使用+号连接得到的,而s1和s2都是变量
        //有变量参与拼接字符串,那么就不会使用常量池了
        System.out.println(s3 == s4);//false

注意,使用"+"拼接字符串也会利用字符串的常量池,但是参与+号拼接的必须时双引号的形式才可以

案例4:

final String s1 = "a";
final String s2 = "b";
//使用+拼接的字符串也会利用字符串常量池
//但是有要求:参与+号拼接的必须是双引号的形式才可以。
String s3 = "a"+"b";
String s4 = s1+s2;
System.out.println(s3.equals(s4));//true
//s3指向的是常量池中的"ab"对象
//s4指向的是常量池中的"ab"对象
//因为s4是由s1和s2使用+号连接得到的,而s1和s2都是final修饰的【常量】
//常量是固定不会变的,在编译期间就能计算出s4的值
//这时候又可是有到字符串常量池了
System.out.println(s3 == s4);//true

注意,final修饰的变量既为常量,常量的拼接在编译期间就可以得出结果,存放到字符串常量池中。

String s1 = "a";
String s2 = "b";
//使用+拼接的字符串也会利用字符串常量池
//但是有要求:参与+号拼接的必须是双引号的形式才可以。
String s3 = "a"+"b";
String s4 = (s1+s2).intern();
System.out.println(s3.equals(s4));//true
//s3指向的是常量池中的"ab"对象
//s4指向的是常量池中的"ab"对象
//intern方法可以在JVM在运行期间,强行使用字符串常量池
//检查当前调用intern方法的字符串,是否在常量池中,如果在那么就返回常量池中的这个对象,如果不在,
那么就把当前这个调用intern方法的字符串存到常量池,供后面的代码使用。
System.out.println(s3 == s4);//true

当调用intern()方法时,JVM会将字符串添加到常量池中,并返回指向该常量的引用

一个单例模式

注意点:

  • 构造器私有;
  • 饿汉式立即加载对象,可能会造成空间浪费,一般不使用
package com.example.demo.single;

/**
 * @Description: 饿汉式单例
 * @author: ZYQ
 * @date: 2021/3/25 10:53
 */
public class Hungry {

    /**
     * 浪费空间
     */
    /*private byte[] data1 = new byte[1024 * 1024];
    private byte[] data2 = new byte[1024 * 1024];
    private byte[] data3 = new byte[1024 * 1024];
    private byte[] data4 = new byte[1024 * 1024];*/

    //保证构造器私有
    private Hungry() {

    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance() {
        return HUNGRY;
    }

}

饿汉式

DCL懒汉式

注意点:

  1. 构造器私有;
  2. 需要使用双重检测,若为单单次检测,则多线程可能同时进入if中,照成多次构造;
  3. 需要对 lazyMan 对象添加 volatile 修饰词。因此new对象的过程不是原子性的,其分为3步:1.为对象分配内存空间 2.初始化对象 3.将实例指向该空间。在此过程中可能会发生指令重排导致第三步优先于第二步执行。当第三步执行完而还未进行初始化时,若此时有其余线程来到第一层判断,则会误判为lazyMan对象非空,从而直接返回一个空的lazyMan,造成错误。
package com.example.demo.single;

/**
 * @Description: 懒汉式单例
 * @author: ZYQ
 * @date: 2021/3/25 11:05
 */
public class LazyMan {

    //构造器私有
    private LazyMan() {
        System.out.println("构造1次");
    }
    
    private static volatile LazyMan lazyMan = null;

    public static LazyMan getInstance() {
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                LazyMan.getInstance();
            }).start();
        }
    }
}

DCL懒汉式

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值