Java学习8:面向对象编程(高级)

8.面向对象编程(高级)

8.1 类变量

概念:类变量也叫静态变量/静态属性,是该类的所有对象共享的变量,任何一个类的对象去访问它时,取到的是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量。

不加static的变量叫法:普通变量/实例变量/非静态变量

Java7 之前,类的静态变量存放在方法区;Java7 之后,将类变量的存储转移到了堆。

定义:

访问修饰符 static 数据类型 变量名;//常用
static 访问修饰符 数据类型 变量名;

访问:

类名.类变量名//推荐
对象名.类变量名

细节讨论:

  1. 类变量是该类的所有对象共享的,而实例变量是每个对象独享的。
  2. 类变量是在类加载时就初始化了,也就是说,即使你没有创建对象,只要类加载了,就可以使用类变量了。
  3. 类变量的生命周期是随类的加载开始,随着类消亡而销毁。
  4. 类变量的访问修饰符的访问权限和范围和实例变量是一样的。

8.2 类方法

类方法也叫静态方法。

// 定义语法:
访问修饰符 static 返回数据类型 方法名(){} // 推荐
static 访问修饰符 返回数据类型 方法名(){}

// 调用方法
类名.类方法名   // 推荐
对象名.类方法名

细节讨论:

  1. 类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区。
  2. 类方法中不允许使用和对象有关的关键字,比如this和super。
  3. 静态方法,只能访问静态成员,非静态方法,可以访问静态成员和非静态成员。

8.3 深入了解main方法

main方法注意事项:main()方法时静态方法,所以我们可以直接调用main方法所在类的静态方法或静态属性,但不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,通过这个对象去访问类中的非静态成员。

解释main方法:

public static void main(String[] args){}
  1. main方法由java虚拟机调用
  2. java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public
  3. java虚拟机在执行main()方法时不必创建对象,所以该方法必须是static
  4. 该方法接收String类的数组参数,该数组中保存执行java命令时传递所运行的类的参数
  5. 传参方式:java 执行的程序 参数1 参数2 参数3

8.4 代码块

代码块又称初始化块,属于类中的成员,类似于方法,将逻辑语句封装在方法体中,通过{}包围起来。但和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显示调用,而是加载类时,或创建对象时隐式调用。

代码块相当于另一种形式的构造器(对构造器的补充机制),可以做初始化的操作,如果多个构造器中都有重复的语句,可以抽取到代码块中,提高代码的复用性。

基本语法:

[修饰符(可选)]{
    代码
};

说明:

  1. 修饰符可选,要写的话,只能写 static
  2. 代码块分为两类,使用static修饰的叫静态代码块,没有static修饰的,叫普通代码块/非静态代码块
  3. 代码块中代码可以为任何逻辑语句(输入、输出、方法调用、循环、判断等)
  4. ;可以写,可以不写

代码块细节:

  1. 静态代码块作用是对类进行初始化,随着类的加载而执行,并且只会执行一次;如果是普通代码块,每创建一个对象就执行一次
  2. 类什么时候被加载
    1. 创建对象实例时(new)
    2. 创建子类对象时,父类也会被加载(父类先被加载,子类后被加载)
    3. 使用类的静态属性时(静态属性、静态方法),父类也会被加载
  3. 如果只使用静态成员时,普通代码块不会被执行。
  4. 创建一个类的调用顺序
    1. 调用静态代码块和静态属性初始化。其中,静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态属性初始化,则按他们定义的顺序调用
    2. 调用普通代码块和普通属性的初始化。其中,普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和多个普通属性初始化,则按定义顺序调用
    3. 调用构造方法。
  5. 构造器的最前面其实隐含了super()和调用普通代码块

总结:创建一个子类的调用顺序如下

  1. 父类的静态代码块和静态属性(优先级一样,按定义顺序执行)
  2. 子类的静态代码块和静态属性(优先级一样,按定义顺序执行)
  3. 父类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
  4. 父类构造方法
  5. 子类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
  6. 子类构造方法

注意:静态代码块只能直接调用静态成员,普通代码块可以调用任意成员

练习

分析下面代码的输出结果

public class CodeBlock {
    public static void main(String[] args) {
        Dog dog = new Dog();
        System.out.println("========");
        Dog dog1 = new Dog();
        System.out.println("========");
        System.out.println(Dog.name);
        System.out.println("========");
        Cat cat = new Cat();
        System.out.println("========");
        System.out.println(Cat.name.name.name.name.a);
    }
}
class Animal{
    {
        System.out.println("父类代码块");
    }
    static {
        System.out.println("父类静态代码块");
    }

    public Animal() {
        System.out.println("父类构造器");
    }
}
class Dog extends Animal{
    public static String name = "大黄";
    {
        System.out.println("子类代码块被执行");
    }
    static {
        System.out.println("子类静态代码块执行");
    }
    {
        System.out.println("子类第二个代码块执行");
    }

    public Dog() {
        System.out.println("构造器被执行");
    }
}
class Cat{
    public static Cat name = new Cat();
    public int a = setA();

    {
        System.out.println("cat代码块");
    }

    public Cat() {
        System.out.println("cat构造器");
    }
    public int setA(){
        System.out.println("设置a的值为5");
        return 5;
    }
}

输出结果为:

父类静态代码块
子类静态代码块执行
父类代码块
父类构造器
子类代码块被执行
子类第二个代码块执行
构造器被执行
========
父类代码块
父类构造器
子类代码块被执行
子类第二个代码块执行
构造器被执行
========
大黄
========
设置a的值为5
cat代码块
cat构造器
设置a的值为5
cat代码块
cat构造器
========
5

结果分析:

区域一

Dog dog = new Dog();
  1. 类加载:main方法中,创建对象dog时,先加载类信息。加载Animal类——父类静态代码块执行;然后加载Dog类——子类静态代码块执行
  2. 父类构造器:进入Dog类的构造器中,先进入隐藏的super(),进入Animal类中,进入Animal类的构造器,执行super()无结果,然后执行Animal类的普通代码块和普通属性初始化——父类代码块执行,接着执行Animal类构造器代码——父类构造器执行。Dog构造器的super()执行完毕。
  3. 子类构造器:Dog构造器的super()执行完毕。执行Dog类的普通代码块和普通属性初始化——子类代码块被执行子类第二个代码块执行。接着执行Dog类构造器代码——构造器被执行

区域二

Dog dog1 = new Dog();

Dog类和Animal类已经加载过一次,不用重新加载。重复区域一的2,3

区域三

如果只使用静态成员时,没有创建对象,普通代码块不会被执行;并且Animal类Dog类已经加载过了,也不执行静态代码块,因此只输出大黄

区域四

  1. 类加载:创建cat对象,先加载Cat类,执行静态代码块和初始化静态属性。按照定义顺序,先初始化静态属性name,name又创建了一个Cat对象,这次创建不必加载类,直接执行构造器中的内容,先执行构造器隐藏部分,super()无结果,执行普通代码块和普通属性初始化,按照定义循序,先初始化普通属性a,调用了setA()方法——设置a的值为5;然后执行普通代码块——cat代码块。构造器隐藏部分执行完毕,接着执行构造器代码——cat构造器
  2. 构造器:构造器内容执行过程和 1 中相同。

区域五

结果很好懂,但有遗留问题:多层的name是什么时候初始化的?

8.5 单例模式

所谓单例模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

单例模式有两种方法:1.饿汉式 2.懒汉式

饿汉式

饿汉式实现步骤:

  1. 将构造器私有化(防止new对象)
  2. 在类的内部直接创建对象(对象是static)
  3. 提供一个公共的static方法,返回创建的对象

代码实现

class GirlFriend{
    private String name;
    
    // 2.在类的内部直接创建对象(对象是static)
    private static GirlFriend girlFriend = new GirlFriend("中野晴");
	
    // 1.将构造器私有化
    private GirlFriend(String name) {
        this.name = name;
    }
	
    // 3.提供一个公共的static方法,返回创建的对象
    public static GirlFriend getInstance(){
        return girlFriend;
    }
}

懒汉式

懒汉式实现步骤

  1. 将构造器私有化
  2. 在类的内部定义对象(对象是static),不初始化
  3. 提供一个公共的static方法,初始化并返回定义的对象

代码实现

class GirlFriend{
    private String name;
    
    // 2.在类的内部定义对象(对象是static),不初始化
    private static GirlFriend girlFriend;
	
    // 1.将构造器私有化
    private GirlFriend(String name) {
        this.name = name;
    }
	
    // 3.提供一个公共的static方法,初始化并返回定义的对象
    public static GirlFriend getInstance(){
        if(girlFriend == null){
            girlFriend = new GirlFriend("中野晴")
        }
        return girlFriend;
    }
}

饿汉式和懒汉式对比

  1. 二者最主要的区别在于创建对象的时机不同:饿汉式是在类加载就创建了对象实例,而懒汉式是在调用getInstance()方法时才创建。
  2. 饿汉式不存在线程安全问题,懒汉式存在线程安全问题。因为有可能多个线程同时进入getInstance()方法获取实例。
  3. 饿汉式存在浪费资源的可能。因为如果没有使用实例,那么饿汉式直接创建的实例对象就浪费了。懒汉式是使用时才创建,不存在这个问题。

8.6 final关键字

fianl可以修饰类、属性、方法和局部变量。

使用final的情况:

  1. 修饰类:被修饰的类不能被继承
  2. 修饰方法:被修饰的方法不能被子类覆盖/重写
  3. 修饰属性:不能被修改
  4. 修饰局部变量:不能被修改

final使用细节:

  1. final修饰的属性又叫常量,一般用XX_XX_XX来命名

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

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

  4. final类不能被继承,但是可以实例化对象

  5. 如果类不是final类,但是含有final方法,该方法虽然不能被重写,但是能被继承使用

  6. final类中的方法没必要再使用final修饰

  7. final不能修饰构造器

  8. final和static搭配使用,不会导致类加载(底层编译器做了优化处理)

    public class Tset {
        public static void main(String[] args) {
            // 静态代码块不会执行
            System.out.println(A.n); // 10
        }
    }
    
    class A {
        public static final int n = 10;
        static {
            System.out.println("静态代码块");
        }
    }
    
  9. 包装类、String类都是final类

8.7 抽象类

当父类的一些方法不能确定时,可以用abstract关键字来修饰该方法,这个方法就是抽象方法,用abstract来修饰的该类就是抽象类。

定义语法

抽象类

访问修饰符 abstract 类名 {}

抽象方法

访问修饰符 abstract 返回类型 方法名(参数列表); // 没有方法体

细节讨论

  1. 抽象类不能被实例化
  2. 抽象类不一定要包含abstract方法。
  3. 包含abstract方法的类必须声明为abstract
  4. abstract只能修饰类和方法,不能修饰属性和其他的
  5. 抽象方法不能有主体
  6. 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非它自己也声明为abstract类。
  7. 一般来说,抽象类会被继承,由其子类来实现抽象方法
  8. 抽象方法不能使用private、final和static来修饰,因为这些关键字都是和重写相违背的。

8.8 接口

接口就是给一些没有实现的方法,封装到一起,到某个类要使用的时候,再根据具体情况把这些方法写出来

在jdk7.0前,接口里的所有方法都是抽象方法,jdk8.0后接口类可以有静态方法、默认方法。

语法:

//定义接口
interface 接口名{
    //属性
    //方法(抽象方法、静态方法、默认实现方法)
}
//实现接口
class 类名 implements 接口{
    //自己的属性
    //自己的方法
    //必须实现接口的抽象方法
}
//调用接口,创建一个方法,参数为接口名
public void num(接口名 db){
    //接口方法
}

细节讨论

  1. 接口不能被实例化

  2. 接口中所有方法都是public方法

  3. 接口中的默认实现方法,需要使用default关键字修饰

    default public void test(){}
    
  4. 接口中的抽象方法可以省略abstract关键字

  5. 如果一个类实现接口,则它必须实现接口的所有抽象方法,除非它是抽象类

  6. 一个类同时可以实现多个接口

    public class C implements A,B{}
    
  7. 接口中的属性,只能是final的,而且是public static final 修饰符。

    int a = 1; // 实际上是 public static final int a = 1;
    
  8. 接口属性的访问形式:接口名.属性名

  9. 一个接口不能继承其他的类,但是可以继承多个别的接口

    interface A extends B,C{}
    
  10. 接口的修饰符和类一样,只能是public和默认

如果子类需要扩展功能,可以通过实现接口的方式扩展,可以把实现接口理解为对Java单继承机制的一种补充

注意:继承和接口同时使用可能会出现二义性,可以使用接口名和super调用来区分,同时使用时先继承,再使用接口,否则会报错

接口的多态性

接口的多态性和类非常相似(接口引用可以指向实现了接口的类的对象),用法也有多态参数、多态数组

特别的,接口存在多态传递现象(其实把接口实现看做继承,和类的多态也一致)

多态传递:接口B继承接口A,类C实现接口B,那么接口A的引用可以指向类C的对象。

8.9 内部类

内部类是类的五大特征中最后一个(属性、方法、构造器、代码块、内部类)

内部类一个类的内部又完整嵌套了另一个类的结构,被嵌套的类称为内部类(inner class),嵌套其他类的类称为外部类。

内部类最大的特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系。

基本语法:

class Outher{//外部类
    class Inner{//内部类
        
    }
}
class Other{//外部其他类
    
}

内部类可以分为四种,两两一组。

  1. 定义在外部类局部位置上(方法、代码块):
    1. 局部内部类
    2. 匿名内部类
  2. 定义在外部类的成员位置上:
    1. 成员内部类
    2. 静态内部类局部内部类
8.9.1 局部内部类

是定义在外部类的局部位置,比如方法中,并且有类名。

定义语法

// 外部类
class Outer{
    // 外部类成员方法
    public void method(){
        // 定义内部类
        class Inner{}
    }
}

细节讨论

  1. 可以直接访问外部类的所有成员
  2. 不能添加访问修饰符(因为它本质是局部变量),但可以用final修饰
  3. 作用域:仅仅在定义它的方法或代码块中
  4. 内部类访问外部类成员:直接访问
  5. 外部类访问内部类成员:先创建内部类实例,再用内部类实例访问
  6. 外部其他类不能访问局部内部类
  7. 如果外部类和局部内部类的成员重名时,遵循就近原则。如果想访问外部类的成员,使用(外部类名.this.成员)访问;外部类名.this本质就是外部类的对象,即哪个对象调用了内部类所在的方法,外部类名.this就是哪个对象

使用示例

public class LocalInnerClass {
    public static void main(String[] args) {
        Outer outer = new Outer("小明");
        outer.test();
        System.out.println(outer);
    }
}

class Outer{
    private String name;

    public Outer(String name) {
        this.name = name;
    }

    private void m1(){
        System.out.println("方法m1");
    }
    public void test(){
        class Inner{
            private String name = "中野晴";
            public void m2(){
                System.out.println("name=" + name + ", OuterName=" + Outer.this.name);
                System.out.println(Outer.this);
                m1();
            }
        }

        new Inner().m2();
    }
}
8.9.2 匿名内部类

匿名内部类和局部内部类一样,都是定义在外部类的局部位置,比如方法中,但没有类名

匿名内部类一般比较类似继承一个其他类、或实现某个接口,主要用途是需要一个只使用一次的类,没有必要单独为只使用一次的类专门写一个类。

先下面代码

class Outer{
    public void method(){
		// tiger 的编译类型:Animal
        // tiger 的运行类型:? Outer$1
        Animal tiger = new Animal(){
            @Override
            public void cry() {
                System.out.println("老虎叫");
            }
        };
        tiger.cry();
    }
}

interface Animal{
    public void cry();
}

在上面代码中,有一个Outer类和Animal接口,在Animal接口中有一个待实现的cry()方法,Outer类中有一个method()方法。

在Outer类的method()方法中定义了一个匿名内部类,匿名内部类没有名字,直接用于创建对象。因为这个匿名内部类实现了Animal接口,所以可以用Animal作为编译类型接收。因此,对象tiger的编译类型是Animal,运行类型是这个没有名字的匿名内部类。

另外,匿名内部类其实是有名字的,类名是:外部类名$number。number从1开始,随创建顺序累加,比如上面的匿名内部类可看作下面的代码(再创建一个匿名内部类的话number会变成2)

class Outer$1 implements Animal{
    @Override
    public void cry() {
        System.out.println("老虎叫");
    }
}

上面用匿名内部类实现接口为例讲解了什么是匿名内部类,匿名内部类继承其他类和继承抽象类实现方法类似。

另一种使用方法

由于jdk底层在创建匿名内部类后立即就会创建它的实例,所以匿名内部类是一个类的定义的同时,本身也是一个对象,因此如果只使用一次的话,匿名内部类可以直接调用方法,不必把创建一个变量接收匿名内部类。因此实现接口的例子可以改写如下代码

class Outer{
    public void method(){
        new Animal(){
            @Override
            public void cry() {
                System.out.println("老虎叫");
            }
        }.cry();
    }
}

interface Animal{
    public void cry();
}

细节讨论部分参考局部内部类

8.9.3 成员内部类

成员内部类定义在外部类的成员位置,并且没有static修饰

定义语法

class Outer{
    private String name = "中野晴";
    private int age = 16;
    class Inner{
        public void m1(){
            System.out.println("name=" + name + ", age=" + age);
        }
    }
    public void test(){
        new Inner().m1();
    }
}

细节讨论

  1. 可以直接访问外部类的所有成员

  2. 可以添加任意访问修饰符(public、protected、默认、private),因为它也是类中的一个成员

  3. 作用域:和外部类其他成员一样

  4. 成员内部类访问外部类成员:直接访问

  5. 外部类访问内部类成员:先创建内部类实例,再用内部类实例访问

  6. 外部其他类访问成员内部类成员:先创建内部类实例,再用内部类实例访问

    // 如何在外部其他类创建内部类实例
    // 法一
    Outer outer = new Outer();
    Outer.Inner inner = outer.new Inner();
    // 法二:在外部类中,编写一个方法,可以返回内部类对象
    Outer.Inner inner = outer.getInnerInstance();
    
  7. 如果外部类和局部内部类的成员重名时,遵循就近原则。如果想访问外部类的成员,使用(外部类名.this.成员)访问;外部类名.this本质就是外部类的对象,即哪个对象调用了内部类所在的方法,外部类名.this就是哪个对象

8.9.4 静态内部类

静态内部类和成员内部类一样,都是定义在外部类的成员位置,但有static修饰

定义语法

class Outer{
    private String name;
    private static int age = 5;

    public static class Inner{
        public void m1(){
//            System.out.println(name);  // 不能访问外部类的非静态成员
            System.out.println(age);
        }
    }
}

细节讨论

  1. 可以直接访问外部类的所有静态成员

  2. 可以添加任意访问修饰符(public、protected、默认、private),因为它也是类中的一个成员

  3. 作用域:和外部类其他成员一样

  4. 外部类访问内部类成员:先创建内部类实例,再用内部类实例访问

  5. 外部其他类访问成员内部类成员:先创建内部类实例,再用内部类实例访问

    // 如何在外部其他类创建内部类实例
    // 法一
    Outer.Inner inner = new Outer.Inner();
    // 法二:在外部类中,编写一个方法,可以返回内部类对象
    Outer outer = new Outer();
    Outer.Inner inner = outer.getInnerInstance();
    
  6. 如果外部类和局部内部类的成员重名时,遵循就近原则。如果想访问外部类的成员,使用(外部类名.成员)访问

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值