Skip to content

S01-10 JavaSE-面向对象-高阶

[TOC]

类变量和类方法

类变量-提出问题

需求:一群小孩玩堆雪人,不时有新小孩加入,如何统计当前玩游戏的总人数?

传统方法的问题

  • 定义独立变量 count 统计,不遵循OOP思想
  • 访问和维护麻烦,不共享于对象

类变量快速入门(ChildGame.java)

java
package com.hspedu.static_;

public class ChildGame {
    public static void main(String[] args) {
        Child child1 = new Child("白骨精");
        Child child2 = new Child("狐狸精");
        Child child3 = new Child("老鼠精");

        child1.join();
        child2.join();
        child3.join();

        // 类变量通过类名访问(推荐)
        System.out.println("共有" + Child.count + " 小孩加入了游戏...");
        System.out.println("child1.count=" + child1.count); // 3
        System.out.println("child2.count=" + child2.count); // 3
        System.out.println("child3.count=" + child3.count); // 3
    }
}

class Child {
    private String name;
    // 类变量(静态变量):所有对象共享
    public static int count = 0;

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

    public void join() {
        System.out.println(name + " 加入了游戏..");
        count++; // 共享变量自增
    }
}

什么是类变量

  • 类变量(静态变量):该类所有对象共享的变量
  • 任何对象访问时,取值/修改的都是同一个变量

类变量定义语法

java
访问修饰符 static 数据类型 变量名; // 推荐
// 或
static 访问修饰符 数据类型 变量名;

类变量访问方式

java
// 1. 类名.类变量名(推荐)
System.out.println(A.name);

// 2. 对象名.类变量名(不推荐,可读性差)
A a = new A();
System.out.println(a.name);

类变量使用注意事项和细节

  1. 适用场景:多个对象共享一个变量时(例:统计学生总缴费 Student.static fee
  2. 类变量 vs 实例变量:
    • 类变量:所有对象共享
    • 实例变量:每个对象独享
  3. 类变量随类加载而初始化(无需创建对象即可访问)
  4. 实例变量不能通过类名访问
  5. 类变量生命周期:随类加载开始,随类消亡而销毁

案例代码(StaticDetail.java)

java
package com.hspedu.static_;

public class StaticDetail {
    public static void main(String[] args) {
        // 类变量无需创建对象即可访问
        System.out.println(B.n2); // 200
        System.out.println(C.address); // 北京

        // 实例变量不能通过类名访问(错误)
        // System.out.println(B.n1);
    }
}

class B {
    public int n1 = 100; // 实例变量
    public static int n2 = 200; // 类变量
}

class C {
    public static String address = "北京"; // 类变量
}

类方法基本介绍

类方法(静态方法)定义语法:

java
访问修饰符 static 数据返回类型 方法名(参数列表) { // 推荐
    // 方法体
}

// 或
static 访问修饰符 数据返回类型 方法名(参数列表) {
    // 方法体
}

类方法调用方式

java
// 1. 类名.类方法名(推荐)
Stu.payFee(100);

// 2. 对象名.类方法名(不推荐)
Stu tom = new Stu("tom");
tom.payFee(200);

类方法应用案例(StaticMethod.java)

java
package com.hspedu.static_;

public class StaticMethod {
    public static void main(String[] args) {
        // 静态方法无需创建对象即可调用
        Stu.payFee(100);
        Stu.payFee(200);
        Stu.showFee(); // 总学费有:300

        // 工具类静态方法调用(示例)
        System.out.println("9 开平方的结果是=" + Math.sqrt(9));
        System.out.println(MyTools.calSum(10, 30)); // 40.0
    }
}

// 工具类:静态方法便于调用
class MyTools {
    // 静态方法:求两数之和
    public static double calSum(double n1, double n2) {
        return n1 + n2;
    }
}

class Stu {
    private String name;
    private static double fee = 0; // 静态变量:累积学费

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

    // 静态方法:访问静态变量
    public static void payFee(double fee) {
        Stu.fee += fee;
    }

    // 静态方法:显示总学费
    public static void showFee() {
        System.out.println("总学费有:" + Stu.fee);
    }
}

类方法经典使用场景

  • 方法不涉及对象相关成员(无 this/super,无实例变量)
  • 工具类方法(例:MathArraysCollections
  • 通用功能(打印数组、排序、计算等)

类方法使用注意事项和细节

  1. 类方法无 this 参数,普通方法隐含 this 参数
  2. 类方法可通过类名/对象名调用;普通方法只能通过对象名调用
  3. 类方法不能使用 this/super 关键字
  4. 类方法只能访问静态成员(静态变量/静态方法)
  5. 普通方法可访问静态成员和非静态成员(遵循访问权限)

案例代码(StaticMethodDetail.java)

java
package com.hspedu.static_;

public class StaticMethodDetail {
    public static void main(String[] args) {
        D.hi(); // 静态方法:直接通过类名调用
        new D().say(); // 普通方法:需创建对象调用
    }
}

class D {
    private int n1 = 100; // 非静态变量
    private static int n2 = 200; // 静态变量

    // 静态方法
    public static void hi() {
        // 错误:不能使用 this
        // System.out.println(this.n1);
        // 正确:访问静态变量
        System.out.println(n2);
        // 正确:调用静态方法
        hello();
        // 错误:调用普通方法
        // say();
    }

    // 静态方法
    public static void hello() {
        System.out.println("静态方法 hello");
    }

    // 普通方法
    public void say() {
        // 正确:访问非静态变量
        System.out.println(n1);
        // 正确:访问静态变量
        System.out.println(n2);
        // 正确:调用静态方法
        hello();
    }
}

课堂练习1(StaticExercise01.java)

java
public class Test {
    static int count = 9;

    public void count() {
        System.out.println("count=" + (count++));
    }

    public static void main(String args[]) {
        new Test().count(); // 输出:9
        new Test().count(); // 输出:10
        System.out.println(Test.count); // 输出:11
    }
}

课堂练习2(StaticExercise02.java)

java
class Person {
    private int id;
    private static int total = 0;

    // 静态方法:返回总人数
    public static int getTotalPerson() {
        // id++;// 错误:静态方法不能访问非静态变量
        return total;
    }

    public Person() { // 构造器
        total++; // 总人数自增
        id = total; // 给id赋值
    }
}

public class TestPerson {
    public static void main(String[] args) {
        System.out.println("Number of total is " + Person.getTotalPerson()); // 0
        Person p1 = new Person();
        System.out.println("Number of total is " + Person.getTotalPerson()); // 1
    }
}

课堂练习3(StaticExercise03.java)

java
class Person {
    private int id;
    private static int total = 0;

    // 静态方法:设置总人数
    public static void setTotalPerson(int total) {
        // this.total = total; // 错误:静态方法不能使用 this
        Person.total = total;
    }

    public Person() { // 构造器
        total++;
        id = total;
    }
}

public class TestPerson {
    public static void main(String[] args) {
        Person.setTotalPerson(3);
        new Person(); // total = 3 + 1 = 4
        System.out.println(Person.getTotalPerson()); // 输出:4
    }
}

理解 main 方法语法

main 方法定义解析

java
public static void main(String[] args) {
    // 方法体
}
  1. 访问权限 public:JVM 需调用,必须公开
  2. static:JVM 调用时无需创建对象
  3. 返回值 void:JVM 无需接收返回值
  4. 参数 String[] args:接收命令行参数(执行 java 类名 参数1 参数2 时传入)

特别提示

  1. main 方法(静态方法)可直接调用本类的静态成员(静态变量/静态方法)
  2. main 方法不能直接访问本类的非静态成员,需创建对象后通过对象访问

案例代码(Main01.java)

java
package com.hspedu.main_;

public class Main01 {
    // 静态变量
    private static String name = "韩顺平教育";
    // 非静态变量
    private int n1 = 10000;

    // 静态方法
    public static void hi() {
        System.out.println("Main01 的hi 方法");
    }

    // 非静态方法
    public void cry() {
        System.out.println("Main01 的cry 方法");
    }

    public static void main(String[] args) {
        // 1. 直接访问静态成员
        System.out.println("name=" + name);
        hi();

        // 2. 不能直接访问非静态成员(错误)
        // System.out.println("n1=" + n1);
        // cry();

        // 3. 访问非静态成员:创建对象后调用
        Main01 main01 = new Main01();
        System.out.println(main01.n1); // ok
        main01.cry(); // ok
    }
}

命令行参数案例(CommandPara.java)

java
package com.hspedu.main_;

public class CommandPara {
    public static void main(String[] args) {
        // 遍历命令行参数
        for (int i = 0; i < args.length; i++) {
            System.out.println("args[" + i + "]= " + args[i]);
        }
    }
}

运行方式

bash
java CommandPara "lisa" "bily" "Mr Brown"

输出结果

args[0]= lisa
args[1]= bily
args[2]= Mr Brown

代码块

基本介绍

  • 代码块(初始化块):类的成员之一,封装逻辑语句({} 包围)
  • 无方法名、无返回值、无参数
  • 无需显式调用,类加载或创建对象时隐式执行

基本语法

java
[修饰符] {
    逻辑语句;
};
  • 修饰符可选,仅能写 static
  • 分号可省略
  • 分类:
    1. 静态代码块(static 修饰)
    2. 普通代码块(无 static 修饰)

代码块的好处和案例

  • 作用:补充构造器,提取多个构造器的重复逻辑,提高代码复用性
  • 执行顺序:代码块优先于构造器执行

案例代码(CodeBlock01.java)

java
package com.hspedu.codeblock_;

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;

    // 普通代码块:提取构造器重复逻辑
    {
        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) {
        System.out.println("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. 静态代码块
    • 类加载时执行,仅执行一次
    • 类加载的3种情况:
      1. 创建对象实例(new
      2. 创建子类对象时,父类先加载
      3. 使用类的静态成员(静态变量/静态方法)
  2. 普通代码块
    • 创建对象时执行,每创建一个对象执行一次
    • 仅创建对象时执行,使用静态成员时不执行

案例代码(CodeBlockDetail01.java)

java
package com.hspedu.codeblock_;

public class CodeBlockDetail01 {
    public static void main(String[] args) {
        // 1. 使用类的静态成员:静态代码块执行,普通代码块不执行
        System.out.println(DD.n1); // 输出:DD 的静态代码1 被执行... → 8888

        // 2. 创建对象:普通代码块执行
        DD dd = new DD(); // 输出:DD 的普通代码块...
        DD dd1 = new DD(); // 输出:DD 的普通代码块...
    }
}

class DD {
    public static int n1 = 8888; // 静态属性

    // 静态代码块
    static {
        System.out.println("DD 的静态代码1 被执行...");
    }

    // 普通代码块
    {
        System.out.println("DD 的普通代码块...");
    }
}
  1. 创建对象时的执行顺序(单个类)
    1. 静态代码块和静态属性初始化(按定义顺序执行)
    2. 普通代码块和普通属性初始化(按定义顺序执行)
    3. 构造方法

案例代码(CodeBlockDetail02.java)

java
package com.hspedu.codeblock_;

public class CodeBlockDetail02 {
    public static void main(String[] args) {
        A a = new A(); 
        // 执行顺序:
        // 1. A 静态代码块01
        // 2. getN1 被调用...(静态属性初始化)
        // 3. A 普通代码块01
        // 4. getN2 被调用...(普通属性初始化)
        // 5. A() 构造器被调用
    }
}

class A {
    // 普通代码块
    {
        System.out.println("A 普通代码块01");
    }

    // 普通属性初始化
    private int n2 = getN2();

    // 静态代码块
    static {
        System.out.println("A 静态代码块01");
    }

    // 静态属性初始化
    private static int n1 = getN1();

    // 静态方法
    public static int getN1() {
        System.out.println("getN1 被调用...");
        return 100;
    }

    // 普通方法
    public int getN2() {
        System.out.println("getN2 被调用...");
        return 200;
    }

    // 构造器
    public A() {
        System.out.println("A() 构造器被调用");
    }
}
  1. 构造器的隐含执行逻辑
    • 构造器最前面隐含 super()(调用父类构造器)
    • 随后调用本类的普通代码块

案例代码(CodeBlockDetail03.java)

java
package com.hspedu.codeblock_;

public class CodeBlockDetail03 {
    public static void main(String[] args) {
        new BBB(); 
        // 执行顺序:
        // 1. AAA 的普通代码块(父类普通代码块)
        // 2. AAA() 构造器被调用....(父类构造器)
        // 3. BBB 的普通代码块...(子类普通代码块)
        // 4. BBB() 构造器被调用....(子类构造器)
    }
}

class AAA { // 父类
    // 普通代码块
    {
        System.out.println("AAA 的普通代码块");
    }

    public AAA() {
        // 隐含 super() 和 普通代码块调用
        System.out.println("AAA() 构造器被调用....");
    }
}

class BBB extends AAA { // 子类
    // 普通代码块
    {
        System.out.println("BBB 的普通代码块...");
    }

    public BBB() {
        // 隐含 super() 和 普通代码块调用
        System.out.println("BBB() 构造器被调用....");
    }
}
  1. 创建子类对象时的执行顺序(继承关系)

    1. 父类的静态代码块和静态属性(按定义顺序)
    2. 子类的静态代码块和静态属性(按定义顺序)
    3. 父类的普通代码块和普通属性(按定义顺序)
    4. 父类的构造方法
    5. 子类的普通代码块和普通属性(按定义顺序)
    6. 子类的构造方法
  2. 代码块访问成员规则

    • 静态代码块:仅能访问静态成员(静态变量/静态方法)
    • 普通代码块:可访问任意成员(静态/非静态)

课堂练习1(CodeBlockExercise01.java)

java
class Person {
    public static int total; // 静态变量

    // 静态代码块
    static {
        total = 100;
        System.out.println("in static block!"); // (1)
    }
}

public class Test {
    public static void main(String[] args) {
        System.out.println("total = " + Person.total); // 100
        System.out.println("total = " + Person.total); // 100
    }
}

输出结果

in static block!
total = 100
total = 100

课堂练习2(CodeBlockExercise02.java)

java
class Sample {
    Sample(String s) {
        System.out.println(s);
    }

    Sample() {
        System.out.println("Sample 默认构造函数被调用");
    }
}

class Test {
    Sample sam1 = new Sample("sam1 成员初始化"); // (3)
    static Sample sam = new Sample("静态成员sam 初始化"); // (1)

    static {
        System.out.println("static 块执行"); // (2)
        if (sam == null) {
            System.out.println("sam is null");
        }
    }

    Test() {
        System.out.println("Test 默认构造函数被调用"); // (4)
    }

    public static void main(String[] args) {
        Test a = new Test();
    }
}

输出结果

静态成员sam 初始化
static 块执行
sam1 成员初始化
Sample 默认构造函数被调用
Test 默认构造函数被调用

单例设计模式

什么是设计模式

  • 设计模式:大量实践中总结的优选代码结构、编程风格、问题解决方案
  • 类似棋谱:针对特定场景的最优解

什么是单例模式

  • 单例模式:保证一个类在整个软件系统中仅存在一个对象实例
  • 核心:
    1. 构造器私有化(防止直接 new
    2. 类内部创建对象
    3. 提供静态公共方法获取对象实例
  • 两种实现方式:饿汉式、懒汉式

单例模式实现

1) 饿汉式(SingleTon01.java)

java
package com.hspedu.single_;

public class SingleTon01 {
    public static void main(String[] args) {
        // 不能直接 new(构造器私有化)
        // GirlFriend xh = new GirlFriend("小红");

        // 通过静态方法获取对象
        GirlFriend instance = GirlFriend.getInstance();
        GirlFriend instance2 = GirlFriend.getInstance();

        System.out.println(instance == instance2); // true(同一对象)
        System.out.println(instance);
        System.out.println(instance2);
    }
}

class GirlFriend {
    private String name;

    // 1. 构造器私有化
    private GirlFriend(String name) {
        System.out.println("构造器被调用...");
        this.name = name;
    }

    // 2. 类内部创建对象(静态变量)
    private static GirlFriend gf = new GirlFriend("小红红");

    // 3. 提供静态公共方法获取对象
    public static GirlFriend getInstance() {
        return gf;
    }

    @Override
    public String toString() {
        return "GirlFriend{" +
                "name='" + name + '\'' +
                '}';
    }
}

2) 懒汉式(SingleTon02.java)

java
package com.hspedu.single_;

public class SingleTon02 {
    public static void main(String[] args) {
        // 首次调用 getInstance() 时创建对象
        Cat instance = Cat.getInstance();
        Cat instance2 = Cat.getInstance();

        System.out.println(instance == instance2); // true
        System.out.println(instance);
        System.out.println(instance2);
    }
}

class Cat {
    private String name;
    public static int n1 = 999;

    // 2. 定义静态变量(默认 null)
    private static Cat cat;

    // 1. 构造器私有化
    private Cat(String name) {
        System.out.println("构造器调用...");
        this.name = name;
    }

    // 3. 提供静态公共方法:使用时才创建对象
    public static Cat getInstance() {
        if (cat == null) {
            cat = new Cat("小可爱");
        }
        return cat;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                '}';
    }
}

饿汉式 VS 懒汉式

对比项饿汉式懒汉式
创建时机类加载时创建使用时(调用 getInstance)创建
线程安全安全(类加载时仅创建一次)不安全(需后续优化)
资源浪费可能浪费(未使用也创建)不浪费(按需创建)

应用场景

  • java.lang.Runtime 类:经典的单例模式实现

final 关键字

基本介绍

  • final 中文意思:最后的、最终的
  • 可修饰:类、属性、方法、局部变量
  • 核心用途:
    1. 修饰类:类不能被继承
    2. 修饰方法:方法不能被重写(override)
    3. 修饰属性:属性值不能被修改(常量)
    4. 修饰局部变量:局部变量值不能被修改

案例代码(Final01.java)

java
package com.hspedu.final_;

public class Final01 {
    public static void main(String[] args) {
        E e = new E();
        // e.TAX_RATE = 0.09; // 错误:常量不能修改
    }
}

// 1. final 修饰类:不能被继承
final class A {}
// class B extends A {} // 错误

class C {
    // 2. final 修饰方法:不能被重写
    public final void hi() {}
}

class D extends C {
    // @Override
    // public void hi() {} // 错误:不能重写 final 方法
}

class E {
    // 3. final 修饰属性:常量(命名规范:全大写,下划线分隔)
    public final double TAX_RATE = 0.08;
}

class F {
    public void cry() {
        // 4. final 修饰局部变量:不能修改
        final double NUM = 0.01;
        // NUM = 0.9; // 错误
        System.out.println("NUM=" + NUM);
    }
}

final 使用注意事项和细节

  1. final 修饰的属性(常量)必须赋初值,赋值位置:
    • 定义时:public final double TAX_RATE = 0.08;
    • 构造器中
    • 代码块中
  2. final 修饰的静态属性,初始化位置:
    • 定义时
    • 静态代码块中(不能在构造器中赋值)
  3. final 类不能继承,但可以实例化对象
  4. final 类中的 final 方法:不能重写,但可以继承
  5. final 不能修饰构造器
  6. finalstatic 搭配使用:效率更高(编译器优化,不触发类加载)
  7. 包装类(IntegerDouble 等)和 String 类都是 final 类(不能被继承)

案例代码(FinalDetail01.java)

java
package com.hspedu.final_;

public class FinalDetail01 {
    public static void main(String[] args) {
        CC cc = new CC(); // final 类可以实例化
        new EE().cal(); // 继承 final 方法并调用
    }
}

// 1. final 属性赋值示例
class AA {
    // 定义时赋值
    public final double TAX_RATE = 0.08;
    // 构造器中赋值
    public final double TAX_RATE2;
    // 代码块中赋值
    public final double TAX_RATE3;

    public AA() {
        TAX_RATE2 = 1.1;
    }

    {
        TAX_RATE3 = 8.8;
    }
}

// 2. final 静态属性赋值示例
class BB {
    public static final double TAX_RATE = 99.9; // 定义时赋值
    public static final double TAX_RATE2;

    static {
        TAX_RATE2 = 3.3; // 静态代码块中赋值
    }
}

// 3. final 类
final class CC {}

// 4. 非 final 类中的 final 方法
class DD {
    public final void cal() {
        System.out.println("cal()方法");
    }
}

class EE extends DD {} // 可以继承,不能重写 cal()

案例代码(FinalDetail02.java)

java
package com.hspedu.final_;

public class FinalDetail02 {
    public static void main(String[] args) {
        // final + static 优化:不触发类加载(静态代码块不执行)
        System.out.println(BBB.num); // 输出:10000(静态代码块未执行)
    }
}

class BBB {
    public final static int num = 10000; // final + static

    static {
        System.out.println("BBB 静态代码块被执行");
    }
}

// 5. final 类无需再修饰方法为 final
final class AAA {
    // public final void cry() {} // 多余
}

应用实例(FinalExercise01.java)

计算圆形面积(圆周率 PIfinal 常量):

java
package com.hspedu.final_;

public class FinalExercise01 {
    public static void main(String[] args) {
        Circle circle = new Circle(5.0);
        System.out.println("面积=" + circle.calArea()); // 78.5
    }
}

class Circle {
    private double radius;
    private final double PI; // final 常量

    // 构造器中赋值
    public Circle(double radius) {
        this.radius = radius;
        PI = 3.14;
    }

    // 计算面积
    public double calArea() {
        return PI * radius * radius;
    }
}

课堂练习

java
// 下面代码是否有误?为什么?
public int addOne(final int x) {
    // ++x; // 错误:final 局部变量不能修改
    return x + 1; // 正确:仅使用值,不修改
}

抽象类

问题引入

父类方法的不确定性:当父类的某些方法无法确定实现逻辑时,直接实现无意义。

java
class Animal {
    private String name;

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

    // 问题:不同动物的 eat() 实现不同,父类无法确定
    public void eat() {
        // System.out.println("这是一个动物,但是不知道吃什么..");
    }
}

解决之道-抽象类

  • abstract 修饰方法:抽象方法(无方法体)
  • abstract 修饰类:抽象类(包含抽象方法的类必须是抽象类)
  • 抽象类的价值:设计规范,由子类继承并实现抽象方法

快速入门代码

java
abstract class class Animal {
    String name;
    int age;

    // 抽象方法(无方法体)
    abstract public void eat();
}

// 子类继承抽象类,必须实现抽象方法
class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }
}

抽象类的基本语法

  1. 抽象类定义:
java
访问修饰符 abstract 类名 {
    // 成员(属性、方法、构造器、静态成员等)
}
  1. 抽象方法定义:
java
访问修饰符 abstract 返回类型 方法名(参数列表); // 无方法体

抽象类使用注意事项和细节

  1. 抽象类不能被实例化(new
  2. 抽象类可以没有抽象方法(仅作为不能实例化的类)
  3. 包含抽象方法的类必须声明为抽象类
  4. abstract 只能修饰类和方法,不能修饰属性和其他
  5. 抽象类可以有任意成员(属性、非抽象方法、构造器、静态成员等)
  6. 抽象方法不能有方法体
  7. 子类继承抽象类:
    • 必须实现所有抽象方法(除非子类也是抽象类)
  8. 抽象方法不能用 privatefinalstatic 修饰(与重写冲突)

案例代码(AbstractDetail01.java)

java
package com.hspedu.abstract_;

public class AbstractDetail01 {
    public static void main(String[] args) {
        // new A(); // 错误:抽象类不能实例化
    }
}

// 1. 抽象类可以没有抽象方法
abstract class A {
    public void hi() {
        System.out.println("hi");
    }
}

// 2. 包含抽象方法的类必须是抽象类
abstract class B {
    public abstract void hi();
}

// 3. abstract 不能修饰属性
class C {
    // public abstract int n1 = 1; // 错误
}

案例代码(AbstractDetail02.java)

java
package com.hspedu.abstract_;

public class AbstractDetail02 {
    public static void main(String[] args) {
        new G().hi(); // 输出:子类实现的 hi()
    }
}

// 4. 抽象类可以有任意成员
abstract class D {
    public int n1 = 10; // 普通属性
    public void hi() { // 普通方法
        System.out.println("hi");
    }
    public abstract void hello(); // 抽象方法
    public static void ok() { // 静态方法
        System.out.println("ok");
    }
}

// 5.