Java语言基础(SE)-第三节 面向对象(二) static、final、嵌套类、局部类、抽象类
static
static 常用来修饰类的成员:成员变量、方法、嵌套类
修饰成员变量、方法
修饰成员变量
- 被 static 修饰:又叫类变量,静态变量,静态字段
- 在程序运行过程中只占用一份固定的内存(存储在方法区)
- 可以通过实例、类访问
- 没有被 static 修饰:实例变量
- 在每个实例内部都有一份内存
- 只能通过实例访问,不可以通过类访问
修饰方法
- 被 static 修饰:又叫类方法、静态方法
- 可以通过实例、类访问
- 内部不可以使用 this
- 可以直接访问类变量、类方法
- 不可以直接访问实例变量、实例方法
- 没有被 static 修饰:实例方法
- 只能通过实例访问,不可以通过类访问
- 内部可以使用 this
- 可以直接访问实例变量、实例方法
- 可以直接访问类变量、类方法
举例使用
- 不推荐使用实例访问类变量、类方法,编译器会发出警告。因此最好通过类直接访问
- 在同一个类中:不能有同名的实例变量和类变量,不能有相同签名的实例方法和类方法
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
静态导入
-
使用了静态导入后,就可以省略类名来访问静态成员(成员变量、方法、嵌套类)
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(); } }
-
静态导入的经典使用场景
package com.zh; import static java.lang.Math.*; public class Main { System.out.println(2*PI*10); }
- 正确使用静态导入,可以消除一些重复的类名,提高代码可读性
- 过度使用静态导入,会让读者分不清静态成员是在哪个类中定义的
- 建议:谨慎使用静态导入
成员变量的初始化
- 编译器会自动为未初始化的成员变量设置初始值
- 如何手动给实例变量提供初始值?
- 在声明中
- 在构造方法中
- 在初始化块中
- 编译器会将初始化块复制到每个构造方法的头部(第一句)(每创建一个实例对象,就会执行一次初始化块)
- 如何手动给类变量提供初始值?
- 在声明中
- 在静态初始化块中
- 当一个类被初始化的时候执行静态初始化块
- 类什么时间被初始化呢? 当一个类第一次被主动使用时,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
- 从打印可以看出,尽管Person被使用了2次,但是static块只执行了一次
- 从这也能看出static块的巨大作用,让某块代码整个应用程序中只执行一次的功能
- 可以有多个(静态)初始化块,按照在源码中出现的顺序被执行
初始化块、静态初始化块
-
查看下面代码的打印顺序
//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();
-
打印结果如下
Preson3 - static block Student - static block Preson3 - block Preson3 - constructor Student - block Student - constructor
-
分析
- 先初始化Student类,肯定先初始化Preson3类
- 先调用Student无参构造,肯定先调用Preson3的无参构造
- Preson3的无参构造肯定先调用初始化块
单例模式(Singleton Pattern)
- 如果一个类设计成单例模式,那么在程序运行过程中,这个类只能创建一个实例
-
饿汉式(不存在线程安全,推荐使用)
//类初始化的时候就创建了Rocket对象 public class Rocket { //1.私有的静态的实例变量 private static Rocket instance = new Rocket(); //2. 构造方法私有化,让外界无法调用构造方法 private Rocket() {} //3. 提供一个公共的静态的,返回一个唯一的那个实例 public static Rocket getInstance() { return instance; } }
-
懒汉式(有线程安全问题)
//懒汉式,有线程安全问题 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
- 被 final 修饰的类:不能被子类化,不能被继承
- 被 final 修饰的方法:不能被重写
- 被 final 修饰的变量:只能进行1次赋值
- 变量包含局部变量(一个函数内部变量)、成员变量、静态成员变量
- 注意如果修饰成员变量,必须在初始化()的时候赋值
- 实例变量: 初始化有3种方式,直接、块、构造函数
- 静态变量:初始化有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)
-
常量的写法
//成员常量,命名为全部大写字母 public static final double PI = 3.14159267; //局部变量常量 static final int AGE = 10;
- 常量的定义就是:
static final 类型 变量名
- 类似于C语言的const
- 常量的定义就是:
-
如果将基本类型或字符串定义为常量,并且在编译时就能确定值
- 编译器会使用常量值替代各处的常量名(类似于 C 语言的宏替换)
- 称为编译时常量( 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)
- 嵌套类:定义在另一个类中的类
- 在嵌套类外层的类,称为:外部类(Outer Class)
-
最外层的外部类,称为:顶级类(Top-level Class)
//顶级类,外部类 public class OuterClass { //静态嵌套类 static class StaticNestedClass { } //非静态嵌套类(内部类) class InnerClass { } }
- 即嵌套类,分为:静态嵌套类、非静态嵌套类(内部类)
内部类(Inner Class)
- 内部类:没有被 static 修饰的嵌套类,非静态嵌套类
- 跟实例变量、实例方法一样,内部类与外部类的实例相关联
- 必须先创建外部类实例,然后再用外部类实例创建内部类实例
- 内部类不能定义除编译时常量以外的任何 static 成员
- 内部类可以直接访问外部类中的所有成员(即使被声明为 private)
-
外部类可以直接访问内部类实例的成员变量、方法(即使被声明为 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(); } }
-
下面代码的内存分布
Person person = new Person(); Hand hand = person.new Hand();
- 堆中先分配一个Person对象内存,存放该对象内存的所有成员变量的值
- 堆中分配一个Hand对象内存,存放该对象内存的所有成员变量的值,而且还存放一个引用值,引用着Person对象的堆地址
-
内部类细节
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)
- 静态嵌套类:被 static 修饰的嵌套类
- 静态嵌套类在行为上就是一个顶级类,只是定义的代码写在了另一个类中
- 对比一般的顶级类,静态嵌套类多了一些特殊权限
- 可以直接访问外部类中的除了实例变量、实例方法以外的其他成员(即使被声明为 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();
}
}
什么情况使用嵌套类?
- 如果类 A 只用在类 C 内部,可以考虑将类 A 嵌套到类 C 中
- 封装性更好、程序包更加简化、增强可读性、维护性
- 如果类 A 需要经常访问类 C 的非公共成员,可以考虑将类 A嵌套到类 C 中
- 另外也可以根据需要将类 A 隐藏起来,不对外暴露
- 如果需要经常访问非公共的实例成员,设计成内部类(非静态嵌套类),否则设计成静态嵌套类
- 如果必须先有 C 实例,才能创建 A 实例,那么可以将 A 设计为 C 的内部类
- 只有内部类的实例先销毁,外部类的实例才会销毁
局部类(Local Class)
- 局部类:定义在代码块中的类(可以定义在方法中、for 循环中、if 语句中等)
- 局部类不能定义除编译时常量以外的任何 static 成员
- 局部类只能访问 final 或者 有效 final 的局部变量
- 从 Java 8 开始,如果局部变量没有被第二次赋值,就认定为是有效 final
- 局部类可以直接访问外部类中的所有成员(即使被声明为 private)
- 局部类只有定义在实例相关的代码块中,才能直接访问外部类中的实例成员(实例变量、实例方法)
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)
- 抽象方法:被 abstract 修饰的实例方法
- 只有方法声明,没有方法实现(参数列表后面没有大括号,而是分号)
- 不能是 private 权限(因为定义抽象方法的目的让子类去实现)
- 只能定义在抽象类、接口中
抽象类(Abstract Class)
- 抽象类:被 abstract 修饰的类
- 可以定义抽象方法
- 不能实例化,但可以自定义构造方法
- 子类必须实现抽象父类中的所有抽象方法(除非子类也是一个抽象类)
- 可以像非抽象类一样定义成员变量、常量、嵌套类型、初始化块、非抽象方法等
- 也就说,抽象类也可以完全不定义抽象方法
- 常见使用场景
- 抽取子类的公共实现到抽象父类中,要求子类必须要单独实现的定义成抽象方法
-
代码举例:
//父类:形状 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