【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);
    }
}
使用注意事项和细节讨论
  1. final修饰的属性又叫常量,一般用XX_XX_XX来命名

  2. final修饰的属性在定义时,必须赋初值,并且以后不能再修改,赋值可以在如下位置之一【选择一个位置赋初值即可】:

    • 定义时:如public final double TAX_RATE=0.08;
    • 在构造器中
    • 在代码块中
  3. 如果final修饰的属性是静态的,则初始化的位置只能是:

    • 定义的时候
    • 在静态代码块,不能在构造器中赋值(因为还没执行到这)
  4. final类不能继承,但是可以实例化对象。[A2类]

  5. 如果类不是final类,但是含有final方法,则该方法虽然不能重写,但是可以被继承。[A3类]

  6. 一般来说,如果一个类已经是final类了,就没有必要再将方法修饰成final方法。

  7. final不能修饰构造方法(即构造器)

  8. final和static往往搭配使用(连着用不会导致类的加载),效率更高,底层编译器做了优化处理。

  9. 包装类(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
//    }
//}
posted @ 2022-11-09 22:33  dayceng  阅读(26)  评论(0编辑  收藏  举报