OOP

面向对象程序设计( Object Oriented Programming,OOP )

三大特性

  • 封装
  • 继承
  • 多态

从编译器的角度看,嵌套包之间毫无关系,( 如:com.jumpig包 和 com.jumpig.joshua包 )

访问控制符:
    - public
        - 所有可见
    - protected:只有内部类
        - 本包和所有子类可见
    - 默认
        - 本包可见
    - private:只有内部类
        - 本类可见
修饰符:
  - abstract
  - final
  - static:只有内部类
    - 为什么顶级类不能用static,只有内部类可以。
    - 我认为顶级类本身就是静态的,因为 Math.abs() 就可以用啊
    - abs() 是静态方法,可以直接类名调用
    - Math.abs() 这样用,也没有构造 Math 对象啊
// import static java.lang.Math.abs;
import static java.lang.Math.*; // 类的静态导入,允许导入静态方法和静态字段
public abstract class Person {
    // 如果构造器中没有初始化,类中的字段(没有final修饰)会自动设置初值(数值为0,布尔值为false,对象为null)
    private String name; // 属性,字段,实例字段,域,成员变量
    private int age = abs(-123);
    private int age = Math.abs(-123); // 若没有静态导入
    
    public Person() { // 构造器
        // 类只有通过构造器才能用,为了构造对象
        // Math.abs()方法为什么能直接调用,因为该方法是 static 修饰
    }
    void Hello(int age) { // 方法
        // 方法中的局部变量必须初始化
        this.name = "张三"; //隐式参数
        age = 18; // 显示参数
    }
}

方法

访问控制符:
    - public
        - 所有可见
    - protect
        - 本包和所有子类可见
    - 默认
        - 本包可见
    - private
        - 本类可见

方法签名

方法签名:方法名、参数类型
两个方法的方法签名不能相同,其他都可以相同。

final

final字段必须在构造对象时初始化。
变量:
  - 对象的引用不会改变。但是值可以改变(如:StringBuilder)
类:
  - 该类不能被继承。
方法:
  - 方法不能被重写。
【注释】:System类中有setOut方法可以修改final字段 public final static PrintStream out = null; 你会奇怪,因为这个是原生的方法,不是用Java实现的。

构造器

如果构造器的第一个语句形如 this(...) ,它将调用同一个类的另一个构造器
                         super(...) ,它将调用父类的构造器

字段

this.  将调用该类的字段
super.  将调用父类的字段(注意访问控制符)

protected

对于 protected 字段、方法 的访问规则一样,为了简便我们只拿字段作为例子。

public class Animal { // 父类
    protected int i = 5;
}

public class Dog extends Animal {
    public void show() {
        Cat cat = new Cat(); // Cat 是不同包中的子类
        // cat.i = 9;   // 无法通过其他子类对象访问父类的protected成员
        // 如果 Cat 在同一个包中,可行。
    }
}

下面在不同的包中

public class Cat extends Animal {
    public void eat() {
        i = 7; // 可以直接访问
    }
}

public class Cat extends Animal {
    public void show() {
        Cat cat = new Cat();
        cat.i = 10; // 通过子类对象访问
        Animal animal = new Cat(); 
        // animal.i  不行
    }
}

public class Cat extends Animal {
    public void show() {
        Animal animal = new Animal();
        // animal.i = 9;   // 无法通过父类对象访问
    }
}

继承

class A {}
class B extends A {}
A : 超类、基类、父类
B : 子类、派生类、孩子类
子类的构造器如果没有显式调用父类构造器,会自动调用父类的无参构造器,如果父类没有无参构造器会报错。
子类字段允许和父类字段一样。
覆盖方法时,子类方法的:
    可见性不能低于超类。如:超类为public,子类也必须为public。
    返回类型不能低于超类。如:超类方法返回Employee对象,子类可以返回Employee或Manager类。

多态

  • 多态是 OOP 技术中最为灵活的特性,它极大的提高了程序的可扩展性和代码的可维护性。

  • 在Java语言中,对象变量是多态的,即对象变量既可以引用本类的对象,也可以引用子类的对象。

  • 在运行时可以选择适当的方法(即:可能选择子类方法也可能选择父类方法),称为动态绑定。

public class Poly {
    public static void main(String[] args) {
        Employee e = new Manager("m1"); // e是一个父类
        e.say();  // 自动调用了子类的方法,而不是父类的,称为动态绑定
        // e.hello(); // Employee类中没有hello()方法
        
        Manager[] managers = new Manager[10];
        Employee[] employees = managers; // 这里managers[0]和employees[0]是相同的引用
        employees[0] = new Employee("");  // 这里编译器竟然接受了这个操作,但运行会出错
    }
}
class Employee {
    private final String name;
    public Employee(String name) { this.name = name; }
    void say() { System.out.println(name); }
    public String getName() { return name; }
}
class Manager extends Employee {
    public Manager(String name) { super(name); }
    @Override
    void say() { System.out.println("--" + getName()); }
    void hello() { System.out.println("Manager特有"); }
}
Employee e;   //员工
Manager m;    //经理
// 经理 继承 员工
e = m;  //向上转型是自动进行的
m = e;  //编译报错
m = (Manager)e;  //强制向下转型

// 强制向下转型可能会失败,强制向下转型前用instanceof判断
if (e instanceof Manager) {
    m = (Manager)e;
}

多态规则

  • 如果父类和子类有相同的方法,会调用子类方法。什么叫做相同的方法呢?
    • 方法名:方法名要一样
    • 参数类型:参数要完全一样,参数类型就算存在子类父类的关系也不行
    • 返回类型:子类方法的返回类型 是 父类方法的返回类型的 子类,或返回类型相同

抽象类

  • 抽象类可以不包含抽象方法
  • 包含抽象方法的类必须是抽象类
  • 抽象类不能实例化,但是允许有构造器,匿名内部类可以使用这个构造器
  • 最多只能继承一个类(可以是抽象类或普通类)
  • 可以不覆盖接口中的抽象方法
  • 抽象方法就是为了重写,所以不能是 private 修饰。

接口

  • 接口中的方法默认 public abstrat ,字段 public static final 。Java 9 可以使用 private 方法。
  • 实现接口时必须把方法声明为 public ,否则指出你试图提供更严格的访问权限。
  • Java 8 可以让接口为 default,可以不重写 default 的方法。
  • Java 8 允许方法用 static 修饰。
  • 一个接口扩展(extends)多个接口。可以覆盖其他接口中的方法。
  • 接口没有构造器,不能用 new 运算符实例化一个接口。
  • 但是接口也是继承 Object 类,可以重写 Object 方法。(这是有点奇怪哦)
  • 接口必须引用实现了这个接口的类对象。
  • 可以用 instanceof 检查一个对象是否实现了某个接口。
  • 任何实现了接口的类都自动地继承了接口中的常量,可以直接使用。

Java 8 可以让接口为 default,可以不重写 default 的方法。

官方文档

Java 8 允许给接口添加一个非抽象的方法实现,只需要使用 default 关键字即可,这个特征又叫做扩展方法(也称为默认方法或虚拟扩展方法或防护方法)。在实现该接口时,该默认扩展方法在子类上可以直接使用,它的使用方式类似于抽象类中非抽象成员方法。

Note:扩展方法(接口中的default方法)不能够重写 Object 中的方法,却可以重载 Object 中的方法。
eg:toString、equals、hashCode 不能在接口中被覆盖,却可以被重载。
Note:
JVM平台的接口的默认方法实现是很高效的,并且方法调用的字节码指令支持默认方法。默认方法使已经存在的接口可以修改而不会影响编译的过程。java.util.Collection中添加的额外方法就是最好的例子:stream(), parallelStream(), forEach(), removeIf()

Java 8 允许方法用 static 修饰。

官方文档

// 可以查看下文中static的使用
static void a() {}

接口必须引用实现了这个接口的类对象。

interface A {}
class B implements A {}
A a = new B();

重载

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表,则视为重载。

// 华为的面试题中曾经问过这样一个问题  “为什么不能根据返回类型来区分重载”,快说出你的答案吧!
// 因为调用时不能指定类型信息,编译器不知道你要调用哪个函数。如:
float max(int a, int b);
int max(int a, int b);
// 有时候编译器可以判断出要调用的是哪个方法。如:
int great = max(3, 4); // 可以判断出要调用的是 int max(int a, int b);
// 有时不能判断出,如:
void max();
int max();
当我们只调用 max(); 时就无法判断要调用什么了。

static

静态方法不能被覆盖。如果父类中定义的静态方法在子类中被重新定义,那么在父类中定义的静态方法将被隐藏。它们的行为也并不具有多态性

可以使用语法:父类名.静态方法调用隐藏的静态方法。

static的执行顺序

执行顺序

static块 > anonymous block > constructor function

直接上代码

//main函数的代码如下
public static void main(String[] args) {
	new ForStatic();
	System.out.println("------------------------");
	new ForStatic(); // static块只会被执行一次,这次就不执行了
}
//类方法,被main方法调用
class ForStatic {
	static {
		System.out.println("111111111     Static 块");
	}
	{
		System.out.println("2222222222    Anonymous block");
	}
	public ForStatic() {
		System.out.println("3333333333     Constructor function");
	}
	static {
		System.out.println("444444444     Static 块");
	}
	{
		System.out.println("555555555     Anonymous block");
	}
}

运行结果

111111111     Static 块
444444444     Static 块
2222222222    Anonymous block
555555555     Anonymous block
3333333333     Constructor function
------------------------
2222222222    Anonymous block
555555555     Anonymous block
3333333333     Constructor function

存在继承的情况下,初始化顺序为:

  1. 父类(静态变量、静态语句块)

  2. 子类(静态变量、静态语句块)

  3. 父类(实例变量、普通语句块)

  4. 父类(构造函数)

  5. 子类(实例变量、普通语句块)

  6. 子类(构造函数)

enum

  • enum 的类型是一个类,其地位与 class、interface 相同。
  • 枚举被设计成单例模式
  • 所有的枚举类型都是 Enum 的子类。
  • 枚举的构造器总是 private 的,即使省略了访问控制符,也还是 private。
public interface Gender {
    void info();
}
public enum Size implements Gender { // 可以实现接口,但不能继承类
    // SMALL,MEDIUM,LARGE;
    SMALL("小"),  // 自动 public static final 修饰
    MEDIUM("中"),
    LARGE("大"); // 枚举值必须声明在最前面

    private final String chinese;

    Size(String chinese) { // 枚举类型里有值,必须声明相同参数的构造器
        this.chinese = chinese;
    }

    public String getChinese() {
        return this.chinese;
    }

    public static void main(String[] args) {
        for (Size size : Size.values()) { // values()方法
            System.out.println(size.name() + " " + size.getChinese()); // SMALL 小
            // Size.SMALL.name() == Size.SMALL == Size.SMALL.toString()
        }
        System.out.println(Size.SMALL.compareTo(Size.LARGE)); // -2
        System.out.println(Size.SMALL.ordinal()); // 返回常量的位置(从0开始)
        Size size = Enum.valueOf(Size.class, "SMALL"); // toString()的逆方法
        Size.SMALL.info(); // 接口方法
    }
    
    @Override
    public void info() { System.out.println("Gender在这里"); }
}

----------------------------------------------------------------------------
============================================================================
    jad反编译后如下,其中,我删了一些没什么用的
    jumpig/lee/test 代码在这个包下
    
public final class Size extends Enum
        implements Gender
{

    public static Size[] values()
    {
        return (Size[])$VALUES.clone();
    }

    public static Size valueOf(String name)
    {
        return (Size)Enum.valueOf(jumpig/lee/test/Size, name);
    }

    private Size(String s, int i, String chinese)  // 这个构造器变了,有了 3 个参数
    {
        super(s, i);
        this.chinese = chinese;
    }

    public static final Size SMALL;
    public static final Size MEDIUM;
    public static final Size LARGE;
    private final String chinese;
    private static final Size $VALUES[];

    static  // 静态代码块,一开始就加载
    {
        SMALL = new Size("SMALL", 0, "\u5C0F");
        MEDIUM = new Size("MEDIUM", 1, "\u4E2D");
        LARGE = new Size("LARGE", 2, "\u5927");
        $VALUES = (new Size[] {
                SMALL, MEDIUM, LARGE
        }); // Size.info(); <---- 不行,通过构造器后才能使用 --->  Size.SMALL.info();
    }
}

抽象方法

public enum Operation {
    PLUS {
        @Override
        public double calculate(double x, double y) { return x + y; }
    },
    MINUS {
        @Override
        public double calculate(double x, double y) { return x - y; }
    };
    abstract double calculate(double x, double y);
    public static void main(String[] args) { // main()方法
        Operation.MINUS.calculate(9, 4);
    }
}
  • java.util 中添加了两个新类:EnumMapEnumSet。专门为枚举类型设计的。
  • MyBatis Plus 里面 MatchSegment 类,在 enum 里使用了 lambda 表达式,很有意思

枚举里使用lambda

@FunctionalInterface
public interface MyISql { // 函数式接口
    String getSql();
}
public enum MyKeyword implements MyISql {
    ORDER_BY("ORDER BY"), // public static final MyKeyword ORDER_BY;
    EXISTS("EXISTS");

    private final String keyword;

    MyKeyword(String keyword) { this.keyword = keyword; }

    @Override
    public String getSql() { return this.keyword; }
}
import java.util.function.Predicate;

public enum MySegment {
    ORDER_BY(i -> i == MyKeyword.ORDER_BY), // public static final MyKeyword ORDER_BY;
    EXISTS(i -> i == MyKeyword.EXISTS);

    private Predicate<MyISql> predicate;

    MySegment(Predicate<MyISql> predicate) { // 构造器,参数为lambda表达式
        this.predicate = predicate;
    }
}

--------------------------------------------------------------------
====================================================================
    cfr反编译后:java -jar cfr-0.151.jar MySegment.class

public enum MySegment {
    ORDER_BY(myISql -> myISql == MyKeyword.ORDER_BY),
    EXISTS(myISql -> myISql == MyKeyword.EXISTS);

    private Predicate<MyISql> predicate;

    private MySegment(Predicate<MyISql> predicate) {
        this.predicate = predicate;
    }
}

常量池

常量池:相同的值只存储一份,节省内存,共享访问

基本类型的包装类:

  • Boolean : true,false
  • Byte: -128~127 , Character: 0~127
  • Short, Int, Long: -128~127
  • String: 字符串类型
  • Float, Double: 没有缓存(常量池)

例子举起来

Integer i1 = 127;
Integer i2 = 127;
i1 == i2  (true)   //Integer的范围从-128~127
Integer i3 = 128;
Integer i4 = 128;
i3 == i4 (false)   //Integer的范围从-128~127
基本类型的包装类和字符串有两种创建方式
1. 常量式赋值创建,放在栈内存(将被常量化)
    String name = "abc";
    Integer a = 10;
2. new对象被创建后,放在堆内存(不被常量化)
    Integer c = new Integer(10);
	String d = new String("abc");
这两种对象创建的方式会放在不同的位置

例子如下

int i1 = 10;     
Integer i2 = 10;      // 自动装箱
i1 == i2  (true)      // 自动拆箱,基本类型和包装类型比较,包装类型自动拆箱

Integer i3 = new Integer(10);
i1 == i3   (true)     // 自动拆箱,基本类型和包装类型比较,包装类型自动拆箱

i2 == i3   (false)
// 两个对象比较,比较其地址
// i2是常量,放在栈内存常量池中,i3是new出来的,放在堆内存中

Integer i4 = new Integer(5);
Integer i5 = new Integer(5);
i1==(i4+i5) (true)    i2==(i4+i5) (true)    i3==(i4+i5) (true)
// i4+i5的操作会使i4,i5自动拆箱为基本类型
String s0 = "abcdef";
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
String s4 = new String("abc");
// s0,s1,s2都放在常量池中(栈内存)     s3,s4(堆内存)
s1==s2 (true)     s1==s3 (false)     s3==s4 (false)

String s5 = s1 + "def";    //s1是变量,涉及到变量,编译器不优化
String s6 = "abc" + "def";     //都是常量,编译器自动优化为abcdef
String s7 = "abc" + new String ("def");   //设计的new对象,编译器不优化
s5==s6 (false)    s5==s7 (false)   s6==s7 (false)   s0==s6 (true)

练习

题1

普通类HashMap中有下面一段,怎么可以没有实现全部的方法?

final class KeyIterator extends HashIterator implements Iterator<K> {
    public final K next() { return nextNode().key; }
}
abstract class HashIterator {
    // ...
}

因为 HashIterator 中已经实现了方法。

题2

// 可以直接返回 B 类
class Hei{
    A me() { return new B(); }
}
class B implements A{
}
interface A {
}

对象克隆

拷贝只是引用相同

克隆相当于再创建一个对象

// Object 中的 clone 方法
protected native Object clone() throws CloneNotSupportedException;

如果拷贝对象中的数据字段都是基本类型或数值,当然没有问题。

但是克隆对象包含子对象的引用,拷贝字段就会得到相同子对象的另一个引用,这样原对象和克隆对象依然会共享一些信息。

浅拷贝:如下,没有克隆对象中引用的其他对象。

如果子对象是一个不可变的类(如:String),或者子对象包含不变的常量,或没有被更改,那当然是安全的。

不过,子对象可能是可变的,那就要重新定义 clone 方法来建立一个深拷贝,同时克隆所有子对象(在这里 hireDay 是一个 Date ,是可变的,所以它也必须克隆)。

// 浅拷贝的例子
class Employee implements Cloneable {
    public Employee clone() throws CloneNotSupportedException {
        return (Employee) super.clone();
    }
}
// Cloneable 是一个标记接口,这个接口指示一个类提供了一个安全的 clone 方法。这个接口没有方法,它的 clone 方法是从 Object 中继承来的。
// 如果一个对象请求克隆,但没有实现这个接口,就会生成一个检查型异常

深拷贝:如果要建立深拷贝,还要克隆对象中可变实例字段(这里:Date 是可变的)。

// 深拷贝的例子
class Employee implements Cloneable {
    public Employee clone() throws CloneNotSupportedException {
        Employee cloned = (Employee) super.clone();
        cloned.hireDay = (Date) hireDay.clone();
        return cloned;
    }
}
// 如果在一个对象上调用 clone ,但这个对象类没有实现 Cloneable 接口,Object 类的 clone 方法就会抛出一个 CloneNotSupportedException 异常。在这里,Date 类实现了 Cloneable 接口,所以不会抛异常,但编译器并不了解这一点,因此我们声明了这个异常:public Employee clone() throws CloneNotSupportedException。

所有的数组都有一个 public 的 clone 方法,可以用这个方法建立一个新数组。