【Java复健指南11】OOP高级02-代码块、单例设计和final关键字
代码块
定义
代码化块又称为初始化块,属于类中的成员[即是类的一部分]。
类似于方法,将逻辑语句封装在方法体中,通过{}包围起来。
但和方法不同,没有方法名,没有返回,没有参数,只有方法体,
而且不用通过对象或类显式调用,而是加载类时,或创建对象时隐式调用。
基本语法
[修饰符]{
代码
}
说明注意:
1)修饰符可选,要写的话,也只能写static。
2)代码块分为两类,使用static修饰的叫静态代码块,没有static修饰的,叫普通代码块。
3)逻辑语句可以为任何逻辑语句(输入、输出、方法调用、循环、判断等)。
4)";"号可以写上,也可以省路。
代码块的好处和案例演示
1)相当于另外一种形式的构造器(对构造器的补充机制),可以做初始化的操作
2)如果多个构造器中都有重复的语句,可以抽取到初始化块中,提高代码的重用性
3)代码块的快速入门
public class CodeBlock01 {
public static void main(String[] args) {
Movie movie = new Movie("你好,李焕英");
System.out.println("===============");
Movie movie2 = new Movie("唐探3", 100, "陈思诚");
}
}
class Movie {
private String name;
private double price;
private String director;
// 故意写3个构造器-》重载
//(1) 下面的三个构造器都有相同的语句
//(2) 这样代码看起来比较冗余
//(3) 这时我们可以把相同的语句,放入到一个代码块中,即可
//(4) 这样当我们不管调用哪个构造器,创建对象,都会先调用代码块的内容
//(5) 代码块调用的顺序优先于构造器..
{
System.out.println("电影屏幕打开...");
System.out.println("广告开始...");
System.out.println("电影正是开始...");
};//分号写不写都行
public Movie(String name) {
System.out.println("Movie(String name) 被调用...");
this.name = name;
}
public Movie(String name, double price) {
this.name = name;
this.price = price;
}
public Movie(String name, double price, String director) {
System.out.println("Movie(String name, double price, String director) 被调用...");
this.name = name;
this.price = price;
this.director = director;
}
}
使用细节
1)静态代码块
static代码块也叫静态代码块,作用就是对类进行初始化,而且它随着类的加载而执行,并且只会执行一次。如果是普通代码块,每创建一个对象。就执行。
2)类什么时候被加载[重要]
- 创建对象实例时(new)
- 创建子类对象实例,父类也会被加载
- 使用类的静态成员时(静态属性,静态方法)
案例演示:A类extends B类的静态块
3)代码块调用规则
普通的代码块,在创建对象实例时,会被隐式的调用。被创建一次,就会调用一次。如果只是使用类的静态成员时,普通代码块并不会执行。
public class CodeBlockDetail01 {
public static void main(String[] args) {
//类被加载的情况举例
//1. 创建对象实例时(new)
// AA aa = new AA();
//2. 创建子类对象实例,父类也会被加载, 而且,父类先被加载,子类后被加载
// AA aa2 = new AA();
//3. 使用类的静态成员时(静态属性,静态方法)
// System.out.println(Cat.n1);
//static代码块,是在类加载时,执行的,而且只会执行一次.
// DD dd = new DD();
// DD dd1 = new DD();
//普通的代码块,在创建对象实例时,会被隐式的调用。
// 被创建一次,就会调用一次。
// 如果只是使用类的静态成员时,普通代码块并不会执行
System.out.println(DD.n1);//8888, 静态模块块一定会执行
}
}
class DD {
public static int n1 = 8888;//静态属性
//静态代码块
static {
System.out.println("DD 的静态代码1被执行...");//
}
//普通代码块, 在new 对象时,被调用,而且是每创建一个对象,就调用一次
//可以这样简单的,理解 普通代码块是构造器的补充
{
System.out.println("DD 的普通代码块...");
}
}
class Animal {
//静态代码块
static {
System.out.println("Animal 的静态代码1被执行...");//
}
}
class Cat extends Animal {
public static int n1 = 999;//静态属性
//静态代码块
static {
System.out.println("Cat 的静态代码1被执行...");//
}
}
class BB {
//静态代码块
static {
System.out.println("BB 的静态代码1被执行...");//1
}
}
class AA extends BB {
//静态代码块
static {
System.out.println("AA 的静态代码1被执行...");//2
}
}
小结
1.static代码块是类加载时,执行,只会执行一次
2.普通代码块是在创建对象时调用的,创建一次,调用一次
3.类加载的3种情况,需要记住
4)类的调用顺序
创建一个对象时,在一个类调用顺序是:(重点。难点)
①调用静态代码块和静态属性初始化(注意:静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态变量初始化,则按他们定义的顺序调用)
②调用普通代码块和普通属性的初始化(注意:普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和多个普通属性初始化,则按定义顺序调用)
③调用构造方法。
即:静态代码块-->普通代码块-->构造器
静态代码块与类加载相关,类加载肯定在对象创建之前,所以最先执行
构造方法(构造器)的最前面其实隐含了super()和调用普通代码块
public class CodeBlockDetail03 {
public static void main(String[] args) {
new BBB();
//执行顺序:
// (1)AAA的普通代码块
// (2)AAA() 构造器被调用
// (3)BBB的普通代码块
// (4)BBB() 构造器被调用
}
}
class AAA { //父类Object
{
System.out.println("AAA的普通代码块");
}
public AAA() {
//(1)super()
//(2)调用本类的普通代码块
System.out.println("AAA() 构造器被调用....");
}
}
class BBB extends AAA {
{
System.out.println("BBB的普通代码块...");
}
public BBB() {
//(1)super()
//(2)调用本类的普通代码块
System.out.println("BBB() 构造器被调用....");
}
}
静态相关的代码块,属性初始化,在类加载时,就执行完毕,因此是优先于构造器和普通代码块执行
5)继承下的代码块关系
我们看一下创建一个子类对象时(继承关系),他们的静态代码块,静态属性初始化,
普通代码块,普通属性初始化,构造方法的调用顺序如下:
①父类的静态代码块和静态属性(优先级一样,按定义顺序执行)
②子类的静态代码块和静态属性(优先级一样,按定义顺序执行)
③父类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
④父类的构造方法
⑤子类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
⑥子类的构造方法/面试题
public class CodeBlockExercise02 {
}
class Sample
{
Sample(String s)
{
System.out.println(s);
}
Sample()
{
System.out.println("Sample默认构造函数被调用");
}
}
class Test{
Sample sam1=new Sample("sam1成员初始化");//
static Sample sam=new Sample("静态成员sam初始化 ");//
static{
System.out.println("static块执行");//
if(sam==null)System.out.println("sam is null");
}
Test()//构造器
{
System.out.println("Test默认构造函数被调用");//
}
//主方法
public static void main(String str[])
{
Test a=new Test();//无参构造器
}
}
运行结果:
1.静态成员sam初始化
2.static块执行
3.sam1成员初始化
4.Test默认构造函数被调用
6)
静态代码块只能直接调用静态成员(静态属性和静态方法),普通代码块可以调用任意成员。
单例设计模式(静态方法/属性经典应用)
定义
单例即单个的实例
1.所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法
2.单例模式有两种方式:
- 饿汉式
- 懒汉式
单例模式应用实例
演示饿汉式和懒汉式单例模式的实现。
基本步骤
1)构造器私有化【防止用户直接取new对象】
2)类的内部创建对象
3)向外暴露一个静态的公共方法getInstance
4)代码实现
饿汉式和懒汉式的不同在于是否在类加载时就创建静态对象,后者需要在getInstance方法中加入判断
饿汉式
所谓"饿汉"即不管你用不用某个类的对象,在该类加载时都给你先实例化一个静态的对象,且只能用规定的类方法获取
GirlFriend类
以"女朋友类"举例,这种类我们只希望其有一个对象,并且不能随意实例化对象
因此在类的内部我们提前初始化一个静态对象【不管之后是否会用到】,这个对象在类被加载时就创建,并且只会创建一次,之后不论调用几次都会指向这个静态对象
并且提供一个getInstance()类方法返回静态对象
//只能有一个女朋友
class GirlFriend {
private String name;
//public static int n1 = 100;
//为了能够在静态方法中,返回 gf对象,需要将其修饰为static
//對象,通常是重量級的對象, 餓漢式可能造成創建了對象,但是沒有使用.
private static GirlFriend gf = new GirlFriend("小红红");
//如何保障我们只能创建一个 GirlFriend 对象
//步骤[单例模式-饿汉式]
//1. 将构造器私有化
//2. 在类的内部直接创建对象(该对象是static)
//3. 提供一个公共的static方法,返回 gf对象
private GirlFriend(String name) {
System.out.println("構造器被調用.");
this.name = name;
}
public static GirlFriend getInstance() {
return gf;
}
@Override
public String toString() {
return "GirlFriend{" +
"name='" + name + '\'' +
'}';
}
}
测试类
public class SingleTon01 {
public static void main(String[] args) {
//传统方式会产生多个对象
// GirlFriend xh = new GirlFriend("小红");
// GirlFriend xb = new GirlFriend("小白");
//通过方法可以获取对象
GirlFriend instance = GirlFriend.getInstance();
System.out.println(instance);
//第二次获取的对象仍指向最初的静态对象
GirlFriend instance2 = GirlFriend.getInstance();
System.out.println(instance2);
System.out.println(instance == instance2);//T
//System.out.println(GirlFriend.n1);
//...
}
}
缺点:创建对象没有使用,可能会造成资源浪费
懒汉式
"懒汉"则是不会默认直接创建静态对象,除非用户调用getInstance方法
并且getInstance方法中会判断是否已经创建过静态对象,没有就创建,已有就返回上次的给你
Cat类
//只能养一只猫
class Cat{
private String name;
public static int n1 = 99;
//2.定义一个static静态属性对象
private static Cat cat;
//步骤
//1.还是构造器私有化
private Cat(String name) {
System.out.println("构造器被调用");
this.name = name;
}
//3.提供一个public的static方法,可以返回一个Cat对象
public static Cat getInstance(){
if(cat == null){//如果还没创建静态对象就创建一个
cat = new Cat("tom");
}
return cat;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
'}';
}
}
测试类
public class SingleTon01 {//饿汉式
public static void main(String[] args) {
// System.out.println(Cat.n1);
Cat instance = Cat.getInstance();
System.out.println(instance);
//第二次调用,若cat对象不为空则不会创建新的对象
//直接返回上一次的对象,从而保证单例
Cat instance2 = Cat.getInstance();
System.out.println(instance2);
System.out.println(instance == instance2);
}
}
饿汉式VS懒汉式
- 二者最主要的区别在于创建对象的时机不同:饿汉式是在类加载就创建了对象实例,而懒汉式是在使用时才创建。
- 饿汉式不存在线程安全问题,懒汉式存在线程安全问题。(等到线程内容部分再补充)
- 饿汉式存在浪费资源的可能。因为如果程序员一个对象实例都没有使用,那么饿汉式创建的对象就浪费了,懒汉式是使用时才创建,就不存在这个问题。
- 在javaSE标准类中,java.lang.Runtime就是经典的单例模式。
final关键字
final可以修饰类、属性、方法和局部变量。
应用场景
在某些情况下,程序员可能有以下需求,就会使用到final:
1)当不希望类被继承时,可以用final修饰.【案例演示】
2)当不希望父类的某个方法被子类覆盖/重写(override)时,可以用final关键字修饰。【案例演示:访问修饰符 final返回类型方法名】
3)当不希望类的的某个属性的值被修改,可以用final修饰.【案例演示: publicfinal double TAX RATE=0.08】
4)当不希望某个局部变量被修改,可以使用final修饰【案例演示: final doubleTAX_RATE=0.08 】
public class Final01 {
public static void main(String[] args) {
E e = new E();
//e.TAX_RATE = 0.09;
}
}
//如果我们要求A类不能被其他类继承
//可以使用final修饰 A类
final class A { }
//class B extends A {}
class C {
//如果我们要求hi不能被子类重写
//可以使用final修饰 hi方法
public final void hi() {}
}
class D extends C {
// @Override
// public void hi() {
// System.out.println("重写了C类的hi方法..");
// }
}
//当不希望类的的某个属性的值被修改,可以用final修饰
class E {
public final double TAX_RATE = 0.08;
}
//当不希望某个局部变量被修改,可以使用final修饰
class F {
public void cry() {
//这时,NUM 也称为 局部常量
final double NUM = 0.01;
//NUM = 0.9;
System.out.println("NUM=" + NUM);
}
}
使用注意事项和细节讨论
-
final修饰的属性又叫常量,一般用XX_XX_XX来命名
-
final修饰的属性在定义时,必须赋初值,并且以后不能再修改,赋值可以在如下位置之一【选择一个位置赋初值即可】:
- 定义时:如public final double TAX_RATE=0.08;
- 在构造器中
- 在代码块中
-
如果final修饰的属性是静态的,则初始化的位置只能是:
- 定义的时候
- 在静态代码块,不能在构造器中赋值(因为还没执行到这)
-
final类不能继承,但是可以实例化对象。[A2类]
-
如果类不是final类,但是含有final方法,则该方法虽然不能重写,但是可以被继承。[A3类]
-
一般来说,如果一个类已经是final类了,就没有必要再将方法修饰成final方法。
-
final不能修饰构造方法(即构造器)
-
final和static往往搭配使用(连着用不会导致类的加载),效率更高,底层编译器做了优化处理。
-
包装类(Integer,Double,Float,Boolean等都是final),String也是final类。
public class FinalDetail02 {
public static void main(String[] args) {
System.out.println(BBB.num);
//包装类,String 是final类,不能被继承
}
}
//final 和 static 往往搭配使用,效率更高,不会导致类加载.底层编译器做了优化处理
class BBB {
public final static int num = 10000;
static {
System.out.println("BBB 静态代码块被执行");
}
}
final class AAA{
//一般来说,如果一个类已经是final类了,就没有必要再将方法修饰成final方法
//public final void cry() {}
}
练习题
计算圆的面积
public class FinalExercise01 {
public static void main(String[] args) {
Circle circle = new Circle(2.0);
circle.mianji();
}
}
class Circle{
private double redius;
private final double PI;
//或者private final double PI = 3.14;
public Circle(double redius) {
this.redius = redius;
//PI = 3.14;
}
{
PI = 3.14;
}
public double mianji(){
return PI*redius*redius;
}
}
判断题
public class FinalExercise02 {
public static void main(String[] args) {
}
}
//public class Something {
// public int addOne(final int x) { //下面的代码是否有误,为什么? 1min
// //++x; //错误,原因是不能修改 final x的值
// return x + 1; //这里是可以,因为此时的x并不指向原来被final修饰的x
// }
//}