Java语言基础(SE)-第三节 面向对象(二) static、final、嵌套类、局部类、抽象类

static

static 常用来修饰类的成员:成员变量、方法、嵌套类

修饰成员变量、方法

修饰成员变量

  1. 被 static 修饰:又叫类变量,静态变量,静态字段
    1. 在程序运行过程中只占用一份固定的内存(存储在方法区)
    2. 可以通过实例、类访问
  2. 没有被 static 修饰:实例变量
    1. 在每个实例内部都有一份内存
    2. 只能通过实例访问,不可以通过类访问

修饰方法

  1. 被 static 修饰:又叫类方法、静态方法
    1. 可以通过实例、类访问
    2. 内部不可以使用 this
    3. 可以直接访问类变量、类方法
    4. 不可以直接访问实例变量、实例方法
  2. 没有被 static 修饰:实例方法
    1. 只能通过实例访问,不可以通过类访问
    2. 内部可以使用 this
    3. 可以直接访问实例变量、实例方法
    4. 可以直接访问类变量、类方法

举例使用

  1. 不推荐使用实例访问类变量、类方法,编译器会发出警告。因此最好通过类直接访问
  2. 在同一个类中:不能有同名的实例变量和类变量,不能有相同签名的实例方法和类方法
public class Person {
    //类变量、静态变量、静态字段
    public static int count = 0;
    //实例变量
    public int age;
    //类方法、静态方法
    public static void test1() {
        //内部不可以使用 this
        //this.age = 10;
        //可以直接访问类变量、类方法
        //count = 1;
        test2();
        //不可以直接访问实例变量、实例方法
        //age = 10;
        //test3();
        System.out.println("static void test1");
    }
    public static void test2() {
        System.out.println("static void test2");
    }
    //实例方法
    public void test3() {
        //可以直接访问类变量、类方法
        //count = 20;
        //test2();
        System.out.println("static void test3");
    }
}
//通过类访问
Person.test1();
Person.count = 10;
Person p1 = new Person();
//也可以实例访问,但是会警告
//p1.count = 20;
p1.test1();
//只能通过实例访问
p1.age = 1;
System.out.println(p1.count); //10
Person p2 = new Person();
p2.age = 2;
System.out.println(p2.count); //10

静态导入

  1. 使用了静态导入后,就可以省略类名来访问静态成员(成员变量、方法、嵌套类)

     package com.zh;
     import static com.zh.Person.*;
     public class Main {
         public static void main(String[] args) {
             count = 30;
             //30
             System.out.println(Person.count);
             //static void test2
             test2();
         }
     }
    
  2. 静态导入的经典使用场景

     package com.zh;
     import static java.lang.Math.*;
     public class Main {
         System.out.println(2*PI*10);
     }
    
  3. 正确使用静态导入,可以消除一些重复的类名,提高代码可读性
  4. 过度使用静态导入,会让读者分不清静态成员是在哪个类中定义的
  5. 建议:谨慎使用静态导入

成员变量的初始化

  1. 编译器会自动为未初始化的成员变量设置初始值
  2. 如何手动给实例变量提供初始值?
    1. 在声明中
    2. 在构造方法中
    3. 初始化块
      1. 编译器会将初始化块复制到每个构造方法的头部(第一句)(每创建一个实例对象,就会执行一次初始化块)
  3. 如何手动给类变量提供初始值?
    1. 在声明中
    2. 静态初始化块
      1. 当一个类被初始化的时候执行静态初始化块
      2. 类什么时间被初始化呢? 当一个类第一次被主动使用时,JVM会对类进行初始化
     package com.zh;
     public class Person2 {
         //声明中初始化
         public static int count = 0;
         //声明中初始化
         //public int age = 0;
         public int age;
         //构造函数中
         public Person2(){
             //age = 10;
         }
         //初始化块
         {
             age = 30;
         }
         //静态初始化块
         static {
             count = 20;
             System.out.println("执行了static块");
         }
     }
     //main函数执行
     System.out.println(Person2.count);
     System.out.println(Person2.count);
     //打印结果
     执行了static块
      20
      20
    
    1. 从打印可以看出,尽管Person被使用了2次,但是static块只执行了一次
    2. 从这也能看出static块的巨大作用,让某块代码整个应用程序中只执行一次的功能
  4. 可以有多个(静态)初始化块,按照在源码中出现的顺序被执行

初始化块、静态初始化块

  1. 查看下面代码的打印顺序

     //Person3
     package com.zh;
     public class Person3 {
         static {
             System.out.println("Preson3 - static block");
         }
         {
             System.out.println("Preson3 - block");
         }
         public Person3() {
             System.out.println("Preson3 - constructor");
         }
     }
                
     //Student
     package com.zh;
     public class Student extends Person3 {
         static {
             System.out.println("Student - static block");
         }
         {
             System.out.println("Student - block");
         }
         public Student() {
             System.out.println("Student - constructor");
         }
     }
        
     //main函数执行如下代码
     Student student = new Student();
    
  2. 打印结果如下

     Preson3 - static block
     Student - static block
     Preson3 - block
     Preson3 - constructor
     Student - block
     Student - constructor
    
  3. 分析

    1. 先初始化Student类,肯定先初始化Preson3类
    2. 先调用Student无参构造,肯定先调用Preson3的无参构造
    3. Preson3的无参构造肯定先调用初始化块

单例模式(Singleton Pattern)

  1. 如果一个类设计成单例模式,那么在程序运行过程中,这个类只能创建一个实例
  2. 饿汉式(不存在线程安全,推荐使用)

     //类初始化的时候就创建了Rocket对象
     public class Rocket {
         //1.私有的静态的实例变量
         private static Rocket instance = new Rocket();
         //2. 构造方法私有化,让外界无法调用构造方法
         private Rocket() {}
         //3. 提供一个公共的静态的,返回一个唯一的那个实例
         public static Rocket getInstance() {
             return instance;
         }
     }
    
  3. 懒汉式(有线程安全问题)

     //懒汉式,有线程安全问题
     public class Rocket2 {
         //1.私有的静态的实例变量
         private static Rocket2 instance = null;
         //2. 构造方法私有化,让外界无法调用构造方法
         private Rocket2() {}
         //3. 提供一个公共的静态的,返回一个唯一的那个实例
         public static Rocket2 getInstance() {
             if (instance == null) {
             //有可能n个线程同时执行这一句,就不是创建一次对象了
             instance = new Rocket2();
             }
             return instance;
         }
     }
    

final

  1. 被 final 修饰的:不能被子类化,不能被继承
  2. 被 final 修饰的方法:不能被重写
  3. 被 final 修饰的变量:只能进行1次赋值
    1. 变量包含局部变量(一个函数内部变量)、成员变量、静态成员变量
    2. 注意如果修饰成员变量,必须在初始化()的时候赋值
      1. 实例变量: 初始化有3种方式,直接、块、构造函数
      2. 静态变量:初始化有2种方式,直接、静态块
public final class Cat {
    //final修饰的成员变量,必须在初始化的时候赋值
    //初始化
    public final int count =10;
    public final int age;
    //块初始化
    {
        //age = 10;
    }
    //构造函数初始化
    public Cat() {
        age =10;
    }
    public final void run() {}
}

//局部变量
final int a = 10;
//不可以
//a = 11;

常量(Constant)

  1. 常量的写法

     //成员常量,命名为全部大写字母
     public static final double PI = 3.14159267;
     //局部变量常量
     static final int AGE = 10;
    
    1. 常量的定义就是:static final 类型 变量名
    2. 类似于C语言的const
  2. 如果将基本类型字符串定义为常量,并且在编译时就能确定值

    1. 编译器会使用常量值替代各处的常量名(类似于 C 语言的宏替换)
    2. 称为编译时常量( compile-time constant)
     static final int AGE = 10;
     public static void main(String[] args) {
         //编译时将所有AGE替换为10
         System.out.println(AGE);
         System.out.println(AGE);
         System.out.println(AGE);
     }
    

嵌套类

嵌套类(Nested Class)

  1. 嵌套类:定义在另一个类中的类
  2. 在嵌套类外层的类,称为:外部类(Outer Class)
  3. 最外层的外部类,称为:顶级类(Top-level Class)

     //顶级类,外部类
     public class OuterClass {
         //静态嵌套类
         static class StaticNestedClass {
         }
         //非静态嵌套类(内部类)
         class InnerClass {
         }
     }
    
  4. 即嵌套类,分为:静态嵌套类、非静态嵌套类(内部类)

内部类(Inner Class)

  1. 内部类:没有被 static 修饰的嵌套类,非静态嵌套类
  2. 跟实例变量、实例方法一样,内部类与外部类的实例相关联
    1. 必须先创建外部类实例,然后再用外部类实例创建内部类实例
    2. 内部类不能定义除编译时常量以外的任何 static 成员
  3. 内部类可以直接访问外部类中的所有成员(即使被声明为 private)
  4. 外部类可以直接访问内部类实例的成员变量、方法(即使被声明为 private)

     package com.zh;
     public class Person {
         private int age;
         public int getAge() {
             return age;
         }
         //外部类可以直接访问内部类实例的成员变量、方法(即使被声明为 private)
         public void testx(Hand hand) {
             System.out.println(hand.weight);
             System.out.println(age);
         }
         //内部类
         public class Hand {
             //实例成员
             private int weight;
             //内部类不能定义除编译时常量以外的任何 static 成员
             //不可以定义static成员
             //private static int count;
             //但是可以定义编译时常量
             private static final int count =5;
             public void test() {
                 //内部类可以直接访问外部类中的所有成员(即使被声明为 private)
                 System.out.println(age);
                 System.out.println(weight);
             }
         }
     }
        
     //Main.java文件夹
     package com.zh;
     //必须导入内部类
     import com.zh.Person.Hand;
     public class Main {
         public static void main(String[] args) {
             //必须先创建外部类实例,然后再用外部类实例创建内部类实例
             Person person = new Person();
             Hand hand = person.new Hand();
             Hand han2 = person.new Hand();
             person.testx(hand);
             hand.test();
         }
     }
    
  5. 下面代码的内存分布

     Person person = new Person();
     Hand hand = person.new Hand();
    
    1. 堆中先分配一个Person对象内存,存放该对象内存的所有成员变量的值
    2. 堆中分配一个Hand对象内存,存放该对象内存的所有成员变量的值,而且还存放一个引用值,引用着Person对象的堆地址
  6. 内部类细节

     public class OuterClass {
         private int x =1;
         //非静态嵌套类(内部类)
         class InnerClass {
             private int x =2;
             public void show() {
                 System.out.println(x); //2
                 System.out.println(this.x);//2
                 //访问外部类实例同名的成员变量
                 System.out.println(OuterClass.this.x);//1
             }
         }
     }
    

静态嵌套类(Static Nested Class)

  1. 静态嵌套类:被 static 修饰的嵌套类
  2. 静态嵌套类在行为上就是一个顶级类,只是定义的代码写在了另一个类中
  3. 对比一般的顶级类,静态嵌套类多了一些特殊权限
    1. 可以直接访问外部类中的除了实例变量、实例方法以外的其他成员(即使被声明为 private)
package com.zh;

public class Student {
    private String name;
    private static int count = 0;
    private void test1() {}
    private static void test2() {}
    	
    public static class Car {
        public void test() {
            //不能访问实例成员
            //System.out.println(name);
            //test1();
            //可以访问
            System.out.println(count);
            test2();
            Student st = new Student();
            //st对象可以直接访问其私有成员
            System.out.println(st.name);
        }
    }
}

//Main.java文件夹
package com.zh;
import com.zh.Student.Car;
public class Main {
    public static void main(String[] args) {
        Student student = new Student();
        //静态嵌套类在行为上就是一个顶级类,只是定义的代码写在了另一个类中
        //Student.Car car = new Student.Car();
        //必须导入内部类import com.zh.Student.Car;才能这么写
        Car car = new Car();
    }
}

什么情况使用嵌套类?

  1. 如果类 A 只用在类 C 内部,可以考虑将类 A 嵌套到类 C 中
    1. 封装性更好、程序包更加简化、增强可读性、维护性
  2. 如果类 A 需要经常访问类 C 的非公共成员,可以考虑将类 A嵌套到类 C 中
    1. 另外也可以根据需要将类 A 隐藏起来,不对外暴露
  3. 如果需要经常访问非公共的实例成员,设计成内部类(非静态嵌套类),否则设计成静态嵌套类
    1. 如果必须先有 C 实例,才能创建 A 实例,那么可以将 A 设计为 C 的内部类
    2. 只有内部类的实例先销毁,外部类的实例才会销毁

局部类(Local Class)

  1. 局部类:定义在代码块中的类(可以定义在方法中、for 循环中、if 语句中等)
  2. 局部类不能定义除编译时常量以外的任何 static 成员
  3. 局部类只能访问 final 或者 有效 final 的局部变量
    1. 从 Java 8 开始,如果局部变量没有被第二次赋值,就认定为是有效 final
  4. 局部类可以直接访问外部类中的所有成员(即使被声明为 private)
    1. 局部类只有定义在实例相关的代码块中,才能直接访问外部类中的实例成员(实例变量、实例方法)
package com.zh;
public class Dog {
    private int age;
    //实例相关的代码块
    public void test() {
        //编译器认为局部变量没有二次赋值,就叫做有效final
        int x = 10;
        //一旦二次赋值,TTT内部就不能访问了,x就不是有效final了
        //x = 20;
        class TTT{
            //不可以
            //static int a = 0;
            //可以
            static final int a = 0;
            int b = 0;
            public void test2() {
                //局部类只能访问 final 或者 有效 final 的局部变量
                //一旦x不是有效final,这里就会报错
                System.out.println(x);
                //局部类可以直接访问外部类中的所有成员(即使被声明为 private)
                System.out.println(age);
            }
        }
        TTT ttt = new TTT();
        ttt.test2();
    }
    //实例相关代码块
    {}
    //非实例相关的代码快
    public static void test3() {
        //这里创建的局部类,内部不能访问外部类中的实例成员
    }
    static {}
}

总结:一个方法是否能访问其他类的实例成员,前提条件一定是调用这个方法前一定先创建了其他类的实例!!!

抽象类

抽象方法(Abstract Method)

  1. 抽象方法:被 abstract 修饰的实例方法
    1. 只有方法声明,没有方法实现(参数列表后面没有大括号,而是分号)
    2. 不能是 private 权限(因为定义抽象方法的目的让子类去实现)
    3. 只能定义在抽象类、接口中

抽象类(Abstract Class)

  1. 抽象类:被 abstract 修饰的
    1. 可以定义抽象方法
    2. 不能实例化,但可以自定义构造方法
    3. 子类必须实现抽象父类中的所有抽象方法(除非子类也是一个抽象类)
    4. 可以像非抽象类一样定义成员变量、常量、嵌套类型、初始化块、非抽象方法等
      1. 也就说,抽象类也可以完全不定义抽象方法
  2. 常见使用场景
    1. 抽取子类的公共实现到抽象父类中,要求子类必须要单独实现的定义成抽象方法
  3. 代码举例:

     //父类:形状
     package com.zh;
     //抽象类
     public abstract class Shape {
         protected double area;
         protected double girth; //周长
         public double getArea() {
             return area;
         }
         public double getGirth() {
             return girth;
         }
         public void show() {
             caculate();
             System.out.println(area + "_" + girth);
         }
         //抽象方法,只能是声明,不能有实现
         protected abstract void caculate();
         //只能是实例方法,不能是类方法(static)
         //protected abstract static void test();
     }
        
     //子类:矩形
     package com.zh;    
     public class Rectangle extends Shape {
         private double width;
         private double height;
         public Rectangle(double width, double height) {
             this.width = width;
             this.height = height;
         }
         @Override
         protected void caculate() {
             //计算矩形面积
             area = width * height;
             //周长
             girth = 2 * (width + height);
         }
        
     }
        
     //子类:圆
     package com.zh;
     public class Circle extends Shape {
         private double radius;
         public Circle(double radius) {
             this.radius = radius;
         }
         @Override
         protected void caculate() {
             double half = Math.PI * radius;
             area = half * radius;
             girth = 2 * half;
         }
     }
        
     //main函数
     //抽象类不能被实例化
     //Shape shape = new Shape();
     Rectangle rec = new Rectangle(10,20);
     rec.show();//200.0_60.0
     Circle cirle = new Circle(10);
     cirle.show(); //314.1592653589793_62.83185307179586
    
Table of Contents