多态

155 阅读15分钟

Day9-第六章(续)

6.4 多态

多态是继封装、继承之后,面向对象的第三大特性。

多态的本意就是多种形态,那么如何理解这个多种形态呢?

6.4.1 多态引用的语法

Java规定父类类型的变量可以接收子类类型的对象,这一点从逻辑上也是说得通的。

父类类型 变量名 = 子类对象; //多态引用

父类类型:指子类继承的父类类型,或者实现的父接口类型。

所以说继承是多态的前提

6.4.2 多态引用的表现:编译时类型与运行时类型不一致

编译时类型:编译器识别的类型,编译器在编译过程中以编译时类型对 对象的使用形式进行格式和语法检查;

运行时类型:对象真实的类型,即new对象时的类型,实际完成的功能得由对象的运行时类型决定。

(1)同一个父类变量可以引用多种子类对象:它们编译时类型相同,运行时类型不同

  • 宠物有多种类型,猫、狗、猪等
  • 一个父类有多个子类
  • 同一个父类变量可以引用多种子类对象
Dog,Cat,Pig等都继承Pet类
Huskie(哈士奇)继承Dog类    
​
Pet p = new Dog();
p = new Cat();
p = new Pig();
p = new Huskie();
​
变量p的编译时类型是Pet,但是运行时类型不同。
给p赋值Dog对象时,p的运行时类型就是Dog
给p赋值Cat对象时,p的运行时类型就是Cat
给p赋值Pig对象时,p的运行时类型就是Pig
给p赋值Huskie对象时,p的运行时类型就是Huskie

(2)一个对象可以赋值给不同类型的变量:它们运行时类型相同,编译时类型不同

  • 一只哈士奇狗,可以说它是哈士奇狗,狗,宠物等
  • 一个对象可以赋值给不同类型的变量
  • 一个对象可以有多种编译时类型
Dog继承Pet类
Huskie(哈士奇)继承Dog类   
​
Huskie h = new Huskie();
Dog d = h;
Pet p = h;
​
同一个哈士奇对象,因为赋值给不同类型的变量,它的编译时类型就不同。
赋值给h时,它的编译时类型是Huskie
赋值给d时,它的编译时类型是Dog
赋值给p时,它的编译时类型是Pet宠物类型

6.4.3 多态引用的好处和弊端

1、好处

代码编写更灵活、功能更强大,可维护性和扩展性更好了。

  • 变量赋值更灵活
  • 实现虚方法(可以被子类重写的方法)的动态绑定、虚方法。运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;变量引用的子类对象不同,执行的方法就不同,实现动态绑定。当然,如果子类没有重写该方法,那么仍然执行从父类继承的方法。
public class Pet {
    private String nickname;
​
    public String getNickname() {
        return nickname;
    }
​
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
    public void eat(){
        System.out.println("吃东西");
    }
}

​
public class Dog extends Pet {
    public void watchHouse(){
        System.out.println("看家");
    }
    @Override
    public void eat(){
        System.out.println("狗狗啃骨头");
    }
}

​
public class Cat extends Pet{
    public void catchMouse(){
        System.out.println("抓老鼠");
    }
    @Override
    public void eat(){
        System.out.println("猫咪吃鱼仔");
    }
}

​
public class Pig extends Pet {
    
}
​

​
public class TestPolymorphismGood {
    public static void main(String[] args) {
        Pet p = new Dog();
        p.eat();
​
        p = new Cat();
        p.eat();
​
        p = new Pig();
        p.eat();
    }
}
运行结果:
狗狗啃骨头
猫咪吃鱼仔
吃东西

2、弊端

通过父类类型的变量引用子类对象时,编译时只能调用父类声明的方法,不能调用子类扩展的方法;

public class TestPolymorphismProblem {
    public static void main(String[] args) {
        Pet pet = new Dog();
        pet.eat();
        pet.watchHouse();//不能调用子类扩展的方法
    }
}

image.png

3、如何获取对象的运行时类型呢?

Object类型的变量与除Object以外的任意引用数据类型的对象都多态引用。Java中认定基本数据类型的一维数组是Object类的子类,但不是Object[]数组类的子类。引用数据类型的一维数组才是Object[]数组类的子类。基本数据类型的二维数组是Object[]数组类的子类。


​

​
public class TestObject {
    public static void main(String[] args) {
        Object o1 = "hello";
        Object o2 = new TestObject();
        Object o3 = new Dog();
        Object o4 = new int[5];
//        Object[] o5 = new int[5];//编译报错
        Object[] o6 = new String[5];
        Object[] o7 = new int[4][];
        Object[] o8 = new String[2][];
        Object[][] o9 = new String[2][];
    }
}

java.lang.Object类中有一个方法可以获取对象的运行时类型

public final Class<?> getClass()返回此 Object 的运行时类
public class TestPolymorphism {
    public static void main(String[] args) {
        Dog d = new Dog();//d变量是Dog类型
        Pet p = d;//p变量是Pet类型
        /*
        上面代码中只创建了一个Dog对象,
        如果把这个Dog对象赋值给Dog类型的变量d,编译时它就是Dog类型,运行时也是Dog类型
        如果把这个Dog对象赋值给Pet类型的变量p,编译时它就是Pet类型,运行时还是Dog类型
         */
​
        System.out.println("d对象的运行时类型:" + d.getClass());
        System.out.println("p对象的运行时类型:" + p.getClass());
        
        p = new Cat();//p变量还可以指向Cat对象。
        System.out.println("p对象的运行时类型:" + p.getClass());
    }
}

运行结果:

d对象的运行时类型:class com.atguigu.polymorphism.grammar.Dog
p对象的运行时类型:class com.atguigu.polymorphism.grammar.Dog
​
p对象的运行时类型:class com.atguigu.polymorphism.grammar.Cat

6.4.4 多态的应用

有的时候,我们在设计一个数组、或一个成员变量、或一个方法的形参、返回值类型时,无法确定它具体的运行时类型,只能确定它是某个系列的编译时类型。

1、没有多态的局限性

案例:

(1)声明一个Dog类,包含public void eat()方法,输出“狗狗啃骨头”

(2)声明一个Cat类,包含public void eat()方法,输出“猫咪吃鱼仔”

(3)声明一个Person类,

  • 包含姓名和宠物属性
  • 包含喂宠物吃东西的方法 public void feed(),实现为调用宠物对象.eat()方法
public class Dog {
    public void eat(){
        System.out.println("狗狗啃骨头");
    }
}
public class Cat {
    public void eat(){
        System.out.println("猫咪吃鱼仔");
    }
}
public class Person {
    private String name;
    private Dog dog;
​
    public String getName() {
        return name;
    }
​
    public void setName(String name) {
        this.name = name;
    }
​
    public Dog getDog() {
        return dog;
    }
​
    public void setDog(Dog dog) {
        this.dog = dog;
    }
​
    //feed:喂食
    public void feed() {
        if (dog != null) {
            dog.eat();
        }
    }
​
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + ''' +
                ", dog=" + dog +
                '}';
    }
}

​
public class TestPerson {
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.setName("张三");
        p1.setDog(new Dog());
        p1.feed();
        System.out.println(p1);
​
        Person p2 = new Person();
        p2.setName("李四");
        p2.setDog(new Dog());
        p2.feed();
        System.out.println(p2);
    }
}
​

问题:

    /*
    问题:
    1、从养狗切换到养猫怎么办?   
        修改代码把Dog修改为养猫?
    2、或者有的人养狗,有的人养猫怎么办?  
    3、要是同时养多个狗,或猫怎么办?
    4、要是还有更多其他宠物类型怎么办?
    如果Java不支持多态,那么上面的问题将会非常麻烦,代码维护起来很难,扩展性很差。
    */

2、声明变量是父类类型,变量赋值子类对象

  • 方法的形参是父类类型,调用方法的实参是子类对象
  • 实例变量声明父类类型,实际存储的是子类对象
public class Pet {
    private String nickname;
​
    public String getNickname() {
        return nickname;
    }
​
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
    public void eat(){
        System.out.println("吃东西");
    }
}
​
public class Dog extends Pet {
    public void watchHouse(){
        System.out.println("看家");
    }
    @Override
    public void eat(){
        System.out.println("狗狗啃骨头");
    }
​
    @Override
    public String toString() {
        return "Dog{"+getNickname()+"}";
    }
}
public class Cat extends Pet{
    public void catchMouse(){
        System.out.println("抓老鼠");
    }
    @Override
    public void eat(){
        System.out.println("猫咪吃鱼仔");
    }
​
    @Override
    public String toString() {
        return "Cat{"+getNickname()+"}";
    }
}
public class PersonOwnOnePet {
    private String name;
    private Pet pet;//实例变量声明为父类类型,实际赋值的是子类对象
​
    public String getName() {
        return name;
    }
​
    public void setName(String name) {
        this.name = name;
    }
​
    public Pet getPet() {
        return pet;
    }
​
    public void setPet(Pet pet) {//方法形参声明为父类类型,实参是子类对象
        this.pet = pet;
    }
​
    //feed:喂食
    public void feed() {
        if (pet != null) {
            //这里eat()执行哪个类的eat()方法,是根据pet对象的运行时类型动态绑定的
            pet.eat();
        }
    }
​
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + ''' +
                ", pet=" + pet +
                //这里+pet,会自动调用pet.toString(),至于执行哪个类的toString方法,也是根据pet对象的运行时类型动态绑定的
                '}';
    }
}
​

​
public class TestPersonPet {
    public static void main(String[] args) {
        PersonOwnOnePet p = new PersonOwnOnePet();
        p.setName("张三");
​
        Dog d = new Dog();
        d.setNickname("旺财");
        p.setPet(d);//里面包含多态特性
        System.out.println(p);//里面包含多态特性
        p.feed();//里面包含多态特性
​
        Cat c = new Cat();
        c.setNickname("雪球");
        p.setPet(c);//里面包含多态特性
        System.out.println(p);//里面包含多态特性
        p.feed();//里面包含多态特性
    }
}
​

运行结果:

Person{name='张三', pet=Dog{旺财}}
狗狗啃骨头
Person{name='张三', pet=Cat{雪球}}
猫咪吃鱼仔

3、数组元素是父类类型,元素对象是子类对象

public class PersonOwnPets {
    private String name;
    private Pet[] pets;
​
    public String getName() {
        return name;
    }
​
    public void setName(String name) {
        this.name = name;
    }
​
    public Pet[] getPets() {
        return pets;
    }
​
    public void setPets(Pet[] pets) {
        this.pets = pets;
    }
    public void feed() {
        for(int i=0; i<pets.length; i++) {
            //这里eat()执行哪个类的eat()方法,是根据pets[i]对象的运行时类型动态绑定的
            pets[i].eat();
        }
    }
​
    @Override
    public String toString() {
        String petString = "";
        for (int i=0; i<pets.length; i++){
            //这里+pets[i],会自动调用pets[i].toString(),至于执行哪个类的toString方法,
            // 也是根据pets[i]对象的运行时类型动态绑定的
            if(i==0){
                petString += pets[i];
            }else {
                petString += "," + pets[i];
            }
        }
​
        return "PersonOwnPets{" +
                "name='" + name + ''' +
                ", pets:"  + petString +
                '}';
    }
}
​

​
public class TestPersonPets {
    public static void main(String[] args) {
        PersonOwnPets p = new PersonOwnPets();
        p.setName("张三");
​
        Dog d = new Dog();
        d.setNickname("旺财");
​
        Cat c = new Cat();
        c.setNickname("雪球");
​
        Pet[] pets = new Pet[2];
        //数组的元素类型声明为父类类型,实际存储的是子类对象
        pets[0] = d;
        pets[1] = c;
        p.setPets(pets);
        System.out.println(p);
        p.feed();
    }
}
​

4、方法返回值类型声明为父类类型,实际返回的是子类对象

public class PetShop {
    //返回值类型是父类类型,实际返回的是子类对象
    public Pet sale(String type){
        switch (type){
            case "Dog":
                return new Dog();
            case "Cat":
                return new Cat();
        }
        return null;
    }
}
public class TestPetShop {
    public static void main(String[] args) {
        PetShop shop = new PetShop();
​
        Pet p1 = shop.sale("Dog");
        /*
        p1的编译时类型是Pet,运行时类型是Dog
         */
        p1.setNickname("小白");
        System.out.println(p1);//自动调用Dog类的toString方法
​
        Pet p2 = shop.sale("Cat");
        /*
        p2的编译时类型是Pet,运行时类型是Cat
         */
        p2.setNickname("雪球");
        System.out.println(p2);//自动调用Cat类的toString方法
    }
}

6.4.5 向上转型与向下转型

1、向上转型

(1)什么是向上转型?

  • 让一个子类对象在在编译期间,以父类的类型呈现,就是向上转型。

(2)如何实现向上转型呢?

  • 当把子类对象赋值给父类的变量时,此时通过这个父类变量引用它时,这个子类对象就呈现为“父类的类型”,
Pet p = new Dog();  
//接下来,通过p引用Dog对象,编译期间就呈现为Pet类型
  • 使用强制类型转换的语法(语法上可以,但是实际中很少这么用)
Dog d = new Dog();
如果以((Pet)d)的形式引用Dog对象,编译期间就呈现为Pet类型

(3)为什么要向上转型呢?

父类类型 > 子类类型。很多地方不得不使用父类类型代替子类类型声明变量。

2、向下转型

(1)什么是向下转型?

  • 让一个父类的变量在在编译期间,以子类的类型呈现,就是向下转型。

(2)如何实现向下转型呢?

  • 必须使用强制类型转换的语法
Pet p = new Dog();
Dog d = (Dog)p;
//接下来,通过d引用Dog对象,编译期间就呈现为Dog类型

(3)为什么要向下转型呢?

为了使用子类扩展的成员。

Pet p = new Dog();
Dog d = (Dog)p;
//接下来,通过d引用Dog对象,编译期间就呈现为Dog类型
d.watchHouse();

3、注意问题

(1)无论向上还是向下,都只发生在编译时,对象的运行时类型不会变

首先,一个对象在new的时候创建的是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型不会变。但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。

这个和基本数据类型的转换是不同的。基本数据类型是把数据值copy了一份,相当于有两种数据类型的值。而对象的赋值不会产生两个对象。

(2)向上转型和向下转型只支持父子类之间
Dog dog = "hello";//错误,不能向上也不能向下,不是父子类之间
(3)向上转型向下转型操作编译通过,运行时不一定通过

不是所有通过编译的转型转换都是正确的,可能会发生ClassCastException

关于向上转型运行时发生错误的在接口部分再演示。

下面演示向下转型时在运行时发生错误的情况:

public class ClassCastTest {
    public static void main(String[] args) {
        //没有类型转换
        Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog
​
        //向上转型
        Pet pet = new Dog();//pet的编译时类型是Pet,运行时类型是Dog
        pet.setNickname("小白");
        pet.eat();//可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
//        pet.watchHouse();//不能调用父类没有的方法watchHouse
​
        Dog d = (Dog) pet;
        System.out.println("d.nickname = " + d.getNickname());
        d.eat();//可以调用eat方法
        d.watchHouse();//可以调用子类扩展的方法watchHouse
​
        Cat c = (Cat) pet;//编译通过,因为从语法检查来说,pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确
        //这句代码运行报错ClassCastException,因为pet变量的运行时类型是Dog,Dog和Cat之间是没有继承关系的
    }
}

==结论:==

==转型要编译通过的,两个类型之间有父子类关系即可。==

==转型要运行成功的,要求对象的运行时类型 <= 转换后的类型。==

4、instanceof关键字

为了避免ClassCastException的发生,Java提供了 instanceof 关键字,用来判断对象的类型关系,只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。

变量/匿名对象 instanceof 数据类型 

那么,哪些instanceof判断会返回true呢?

  • 对象的编译时类型 与 instanceof后面数据类型是 父子类关系才编译通过
  • 对象的运行时类型<= instanceof后面数据类型,运行结果才为true

示例代码:


​
public class TestInstanceof {
    public static void main(String[] args) {
        Pet[] pets = new Pet[2];
        pets[0] = new Dog();//多态引用
        pets[0].setNickname("小白");
        pets[1] = new Cat();//多态引用
        pets[1].setNickname("雪球");
​
        for (int i = 0; i < pets.length; i++) {
            pets[i].eat();
​
            if(pets[i] instanceof Dog){
                Dog dog = (Dog) pets[i];
                dog.watchHouse();
            }else if(pets[i] instanceof Cat){
                Cat cat = (Cat) pets[i];
                cat.catchMouse();
            }
        }
    }
}

6.4.6 虚方法的匹配和调用原则

在Java中虚方法是指在编译阶段和类加载阶段都不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。

当我们通过“对象.方法”的形式调用一个虚方法时,要如何确定它具体执行哪个方法呢?

==注意:super.方法 和 对象.非虚方法(静态方法,final修饰的方法等),不使用以下规则。==

(1)编译时静态分派:先看这个对象xx的编译时类型,在这个对象的编译时类型中找到能匹配的方法

匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
A:找最匹配    实参的编译时类型 = 方法形参的类型
B:找兼容      实参的编译时类型 < 方法形参的类型

(2)运行时动态绑定:再看这个对象xx的运行时类型,如果这个对象xx的运行时类重写了刚刚找到的那个匹配的方法,那么执行重写的,否则仍然执行刚才编译时类型中的那个匹配的方法


    
class MyClass{
    public void method(Father f) {
        System.out.println("father");
    }
    public void method(Son s) {
        System.out.println("son");
    }
}
class MySub extends MyClass{
    public void method(Father d) {
        System.out.println("sub--father");
    }
    public void method(Daughter d) {
        System.out.println("daughter");
    }
}
class Father{
    
}
class Son extends Father{
    
}
class Daughter extends Father{
    
}
public class TestVirtualMethod {
    public static void main(String[] args) {
        Father f = new Father();
        Son s = new Son();
        Daughter d = new Daughter();
        
        MyClass my = new MySub();
        my.method(f);//sub--father
            /*
            (1)静态分派:看my的编译时类型MyClass,在MyClass中找最匹配的
                匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
                 A:找最匹配    实参的编译时类型 = 方法形参的类型
                 B:找兼容      实参的编译时类型 < 方法形参的类型
                 实参f的编译时类型是Father,形参(Father f) 、(Son s)
                 最匹配的是public void method(Father f)
            (2)动态绑定:看my的运行时类型MySub,看在MySub中是否有对    public void method(Father f)进行重写
                发现有重写,如果有重写,就执行重写的
                    public void method(Father d) {
                        System.out.println("sub--");
                    }
             */
        my.method(s);//son
            /*
            (1)静态分派:看my的编译时类型MyClass,在MyClass中找最匹配的
                匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
                 A:找最匹配    实参的编译时类型 = 方法形参的类型
                 B:找兼容      实参的编译时类型 < 方法形参的类型
                 实参s的编译时类型是Son,形参(Father f) 、(Son s)
                 最匹配的是public void method(Son s)
            (2)动态绑定:看my的运行时类型MySub,看在MySub中是否有对 public void method(Son s)进行重写
                发现没有重写,如果没有重写,就执行刚刚父类中找到的方法
             */
        my.method(d);//sub--father
             /*
            (1)静态分派:看my的编译时类型MyClass,在MyClass中找最匹配的
                匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
                 A:找最匹配    实参的编译时类型 = 方法形参的类型
                 B:找兼容      实参的编译时类型 < 方法形参的类型
                 实参d的编译时类型是Daughter,形参(Father f) 、(Son s)
                 最匹配的是public void method(Father f)
            (2)动态绑定:看my的运行时类型MySub,看在MySub中是否有对 public void method(Father f)进行重写
                发现有重写,如果有重写,就执行重写的
                    public void method(Father d) {
                        System.out.println("sub--");
                    }
             */
    }
}

思考:

  • 要是把my的类型声明为MySub类型会怎么样
  • 要是把s和d的类型声明为Father类型会怎么样?

6.4.7 成员变量没有多态一说


​
public class Father {
    int a = 1;
    int c = 1;
}

​
public class Son extends Father {
    int a = 2;
    int d = 2;
}
​
public class TestField {
    public static void main(String[] args) {
        Father f = new Son();
        System.out.println("f.a = " + f.a);
​
        System.out.println("f.c = " + f.c);
//        System.out.println("f.d" + f.d);//错误
        System.out.println("((Son)f).a = " + ((Son)f).a );
​
        System.out.println("((Son)f).d = " + ((Son)f).d);
​
        System.out.println("-------------------");
        Son s =  new Son();
        System.out.println("s.a = " + s.a);
​
        System.out.println("s.c = " + s.c);
        System.out.println("s.d = " + s.d);
        System.out.println("((Father)s).a = " + ((Father)s).a);
        System.out.println("((Father)s).c = " + ((Father)s).c);
//        System.out.println("((Father)s).d = " + ((Father)s).d);//错误
    }
}

image-20220315140606478.png

6.5 native关键字(简单了解)

1.native的意义

native:本地的,原生的

2.native的语法

native只能修饰方法,表示这个方法的方法体代码不是用Java语言实现的,而是由C/C++语言编写的。但是对于Java程序员来说,可以当做Java的方法一样去正常调用它,或者子类重写它。

JVM内存的管理:

1561465258546.png

区域名称作用
程序计数器程序计数器是CPU中的寄存器,它包含每一个线程下一条要执行的指令的地址
本地方法栈当程序中调用了native的本地方法时,本地方法执行期间的内存区域
方法区存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆内存存储对象(包括数组对象),new来创建的,都存储在堆内存。
虚拟机栈用于存储正在执行的每个Java方法的局部变量表等。局部变量表存放了编译期可知长度的各种基本数据类型、对象引用,方法执行完,自动释放。

6.6 final关键字

1.final的意义

final:最终的,不可更改的

2.final修饰类

表示这个类不能被继承,没有子类

final class Eunuch{//太监类
    
}
class Son extends Eunuch{//错误
    
}

3.final修饰方法

表示这个方法不能被子类重写

class Father{
    public final void method(){
        System.out.println("father");
    }
}
class Son extends Father{
    public void method(){//错误
        System.out.println("son");
    }
}

4.final修饰变量

final修饰某个变量(成员变量或局部变量),表示它的值就不能被修改,即常量,常量名建议使用大写字母。

如果某个成员变量用final修饰后,没有set方法,并且必须初始化(可以显式赋值、或在初始化块赋值、实例变量还可以在构造器中赋值)

public class TestFinal {
    public static void main(String[] args){
        final int MIN_SCORE = 0;
        final int MAX_SCORE = 100;
​
        MyDate m1 = new MyDate();
        System.out.println(m1.getInfo());
​
        MyDate m2 = new MyDate(2022,2,14);
        System.out.println(m2.getInfo());
    }
}
class MyDate{
    //没有set方法,必须有显示赋值的代码
    private final int year;
    private final int month;
    private final int day;
​
    public MyDate(){
        year = 1970;
        month = 1;
        day = 1;
    }
​
    public MyDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
​
    public int getYear() {
        return year;
    }
​
    public int getMonth() {
        return month;
    }
​
    public int getDay() {
        return day;
    }
​
    public String getInfo(){
        return year + "年" + month + "月" + day + "日";
    }
}

6.7 静态关键字(static)

在类中声明的实例变量,其值是每一个对象独立的。但是有些成员变量的值不需要或不能每一个对象单独存储一份,即有些成员变量和当前类的对象无关。

在类中声明的实例方法,在类的外面必须要先创建对象,才能调用。但是有些方法的调用和当前类的对象无关,那么创建对象就有点麻烦了。

此时,就需要将和当前类的对象无关的成员变量、成员方法声明为静态的(static)。

6.7.1 静态变量

1、语法格式

有static修饰的成员变量就是静态变量。

【修饰符】 class 类{
    【其他修饰符】 static 数据类型  静态变量名;
}

2、静态变量的特点

  • 静态变量的默认值规则和实例变量一样。
  • 静态变量值是所有对象共享。
  • 静态变量的值存储在方法区。
  • 静态变量在本类任意位置可以直接使用。
  • 如果权限修饰符允许,在其他类中可以通过“类名.静态变量”直接访问,也可以通过“对象.静态变量”的方式访问(但是更推荐使用类名.静态变量的方式)。
  • 静态变量的get/set方法也静态的,当局部变量与静态变量重名时,使用“类名.静态变量”进行区分。
分类数据类型默认值
基本类型整数(byte,short,int,long)0
浮点数(float,double)0.0
字符(char)'\u0000'
布尔(boolean)false
数据类型默认值
引用类型数组,类,接口null

演示:

public class Employee {
    private static int total;//这里私有化,只能通过提供的get/set访问
    static String company; //这里缺省权限修饰符,是为了演示在类外面演示“类名.静态变量”的方式访问
    private final int id;
    private String name;
​
    public Employee() {
        id = ++total;
    }
​
    public Employee(String name) {
        id = ++total;
        this.name = name;
    }
​
    public int getId() {
        return id;
    }
​
    public String getName() {
        return name;
    }
​
    public void setName(String name) {
        this.name = name;
    }
​
    public static int getTotal() {
        return total;
    }
​
    public static void setTotal(int total) {
        Employee.total = total;
    }
​
    @Override
    public String toString() {
        return "Employee{company = " + company + ",id = " + id + " ,name=" + name +"}";
    }
}
​
public class TestEmployee {
    public static void main(String[] args) {
        //静态变量total的默认值是0
        System.out.println("Employee.total = " + Employee.getTotal());
​
        Employee c1 = new Employee("张三");
        Employee c2 = new Employee();
        System.out.println(c1);//静态变量company的默认值是null
        System.out.println(c2);//静态变量company的默认值是null
        System.out.println("Employee.total = " + Employee.getTotal());//静态变量total值是2
​
        Employee.company = "尚硅谷";
        System.out.println(c1);//静态变量company的值是尚硅谷
        System.out.println(c2);//静态变量company的值是尚硅谷
​
        //只要权限修饰符允许,虽然不推荐,但是也可以通过“对象.静态变量”的形式来访问
        c2.company = "超级尚硅谷";
        c2.setName("李四");
​
        System.out.println(c1);//静态变量company的值是超级尚硅谷
        System.out.println(c2);//静态变量company的值是超级尚硅谷
    }
}
​

3、静态变量内存分析

image-20220104100145059.png

4、静态类变量和非静态实例变量、局部变量

  • 静态类变量(简称静态变量):存储在方法区,有默认值,所有对象共享,生命周期和类相同,还可以有权限修饰符、final等其他修饰符
  • 非静态实例变量(简称实例变量):存储在堆中,有默认值,每一个对象独立,生命周期每一个对象也独立,还可以有权限修饰符、final等其他修饰符
  • 局部变量:存储在栈中,没有默认值,每一次方法调用都是独立的,有作用域,只能有final修饰,没有其他修饰符

6.7.2 静态方法

1、语法格式

有static修饰的成员方法就是静态方法。

【修饰符】 class 类{
    【其他修饰符】 static 返回值类型 方法名(形参列表){
        方法体
    }
}

2、静态方法的特点

  • 静态方法在本类的任意方法、代码块、构造器中都可以直接被调用。
  • 只要权限修饰符允许,静态方法在其他类中可以通过“类名.静态方法“的方式调用。也可以通过”对象.静态方法“的方式调用(但是更推荐使用类名.静态方法的方式)。
  • 静态方法可以被子类继承,但不能被子类重写。
  • 静态方法的调用都只看编译时类型。
public class Father {
    public static void method(){
        System.out.println("Father.method");
    }
​
    public static void fun(){
        System.out.println("Father.fun");
    }
}

​
public class Son extends Father{
//    @Override //尝试重写静态方法,加上@Override编译报错,去掉Override不报错,但是也不是重写
    public static void fun(){
        System.out.println("Son.fun");
    }
}
public class TestStaticMethod {
    public static void main(String[] args) {
        Father.method();
        Son.method();//继承静态方法
​
        Father f = new Son();
        f.method();//执行Father类中的method
        
        Father f = null;
        f.method();//执行Father类中的method
    }
}

6.7.3 静态和非静态的区别

1、本类中的访问限制区别

静态的类变量和静态的方法可以在本类任意位置直接访问。

非静态的实例变量和非静态的方法==只能==在本类的非静态的方法、构造器等非静态成员中直接访问。

即:

  • 静态直接访问静态,可以
  • 非静态直接访问非静态,可以
  • 非静态直接访问静态,可以
  • 静态直接访问非静态,不可以

2、在其他类的访问方式区别

静态的类变量和静态的方法可以通过“类名.”的方式直接访问;也可以通过“对象."的方式访问。(但是更推荐使用==”类名."==的方式)

非静态的实例变量和非静态的方法==只能==通过“对象."方式访问

6.7.4 静态导入(简单了解)

如果大量使用另一个类的静态成员,可以使用静态导入,简化代码。

import static 包.类名.静态成员名;
import static 包.类名.*;

演示:


​
import static java.lang.Math.*;
​
public class TestStaticImport {
    public static void main(String[] args) {
        //使用Math类的静态成员
        System.out.println(Math.PI);
        System.out.println(Math.sqrt(9));
        System.out.println(Math.random());
​
        System.out.println("----------------------------");
        System.out.println(PI);
        System.out.println(sqrt(9));
        System.out.println(random());
    }
}
​