Java 基础篇专栏 – 类与对象

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

Java 修仙之旅 - 基础篇

类、对象和引用的关系

类和对象的关系

  • 类是对象的模版,对象是类的一个实例,一个类可以有很多对象

  • 一个 Java 程序中类名相同的类只能有一个,也就是类型不会重名

引用和类以及对象的关系

  • 引用只能指向其所属的类型的类的对象

  • 相同类型的引用之间可以赋值

  • 只能通过指向一个对象的引用,来操作一个对象,比如访问某个成员变量

方法

参数传递方式

Java 总是采用按值调用

基本类型的值传递
 1public class PrimitiveTransferTest {
2    public static void swap(int a, int b) {
3        int tmp = a;
4        a = b;
5        b = tmp;
6        System.out.println("swap 方法里 a 的值为: " + a + " b的值为: " + b);
7    }
8
9    public static void main(String[] args) {
10        int a = 6;
11        int b = 9;
12        swap(a, b);
13        System.out.println("交换结束后 a 的值为 " + a + " b的值为 " + b);
14    }
15}
16
17/**
18运行结果:
19swap 方法里 a 的值为: 9 b的值为: 6
20交换结束后 a 的值为 6 b的值为 9
21*/

分析图:

Java 基础篇专栏 - 类与对象




Java 基础篇专栏 - 类与对象

java 程序总是从 main() 方法开始执行,main() 方法定义了 a、b 两个局部变量,两个变量在 main 栈区中。在 main() 方法中调用 swap() 方法时,main() 方法此时还未结束,因此系统为 main 方法和 swap 方法分配了两块栈区,用于保存 main 方法和 swap 方法的局部变量。main 方法中的 a、b 变量作为参数传入 swap 方法,实际上是在 swap 方法栈区中重新产生了两个变量 a、b,并将 main 方法栈区中 a、b 变量的值分别赋给 swap 方法栈区中的 a、b 参数(这就是初始化)。此时系统内存中有两个 a 变量、两个 b 变量,只是存在于不同的方法栈区中而已。

引用类型的参数传递
 1public class ReferenceTransferTest {
2    public static void swap(DataWrap dw) {
3        int tmp = dw.a;
4        dw.a = dw.b;
5        dw.b = tmp;
6        System.out.println("swap 方法里, a 成员变量的的值为: " + dw.a + " b 成员变量的值为: " + dw.b);
7    }
8
9    public static void main(String[] args) {
10        DataWrap dw = new DataWrap();
11        dw.a = 6;
12        dw.b = 9;
13        swap(dw);
14        System.out.println("交换结束后, a 成员变量的的值为: " + dw.a + " b 成员变量的值为: " + dw.b);
15    }
16}
17
18/**
19swap 方法里, a 成员变量的的值为: 9 b 成员变量的值为: 6
20交换结束后, a 成员变量的的值为: 9 b 成员变量的值为: 6
21*/

你可能会疑问,dw 对象的成员变量 a、b的值也被替换了,这跟前面基本类型的传递完全不一样。这非常容易让人觉得,调用传入 swap 方法的就是 dw 对象本身,而不是它的复制品。其实传递的依然是 dw 的值。

分析图:

Java 基础篇专栏 - 类与对象

Java 基础篇专栏 - 类与对象

系统一样赋值了 dw 的副本,只是关键在于 dw 只是一个引用变量,它存储的值只是一段内存地址,将该内存地址传递给 swap 栈区,此时 swap 栈区的 dw 和 main 栈区的 dw 的值也就是内存地址相同,该段内存地址指向堆内存中的 DataWrap 对象。对 swap 栈区的 dw 操作,也就是对 DataWrap 对象操作。

重载

重载:同一个类中,方法名相同,参数列表不同。

当调用被重载的方法时,根据参数的个数和类型判断应该调用哪个重载方法,参数完全匹配的方法将被执行。

构造器

默认无参构造器

仅当类没有定义任何构造器的时候,系统才会提供一个默认的构造器。这个构造器将所有的实例域设置为默认值。

自定义构造器

当类中有自定义构造器时,系统不会再提供默认的构造器

静态常量

1public static final double PI = 3.1415926

静态方法

在类加载的时候就存在了,不依赖于任何类的任何实例。

建议通过类名调用,而不是通过实例对象调用,否则很容易混淆概念。

继承

java 使用 extends 作为继承的关键字,有趣的是 extends 是扩展的意思,并不是继承。但是 extends 很好体现了子类和父类的关系,子类是对父类的扩展,子类是一种特殊的父类。扩展更加准确。ps:这个理解真的是流弊啊。

子类重写父类方法(覆盖)

  • 方法名相同、形参列表相同

子类中调用父类被覆盖的方法

  • 如果被覆盖的方法是实例方法,使用 super 关键字

  • 如果被覆盖的方法是类方法,使用父类类名

  • 子类不能调用父类中被 private 修饰的方法和属性

子类调用父类构造器

  • 子类不能继承父类的构造器。在子类的构造器中,如果没有显式使用 super 调用父类的构造函数,那么系统一定会在子类构造器执行之前,隐式的调用父类的无参构造器

  • 在子类构造器中,可以使用 super 显式调用父类构造器,但 super 语句必须在第一行

多态

向上类型转换

Java 引用变量有两个类型。如果编译时类型和运行时类型不一致,就可能出现多态。

  • 编译时类型:由声明该变量时使用的类型决定

  • 运行时类型:由实际赋给该变量的对象决定

示例代码:

 1public class BaseClass {
2    public int book = 6;
3
4    public void base() {
5        System.out.println("父类的普通方法");
6    }
7
8    public void test() {
9        System.out.println("父类的test方法");
10    }
11}
12
13public class SubClass extends BaseClass {
14    public String book = "轻量级 Java EE";
15
16    public void test() {
17        System.out.println("子类的test方法");
18    }
19
20    public void sub() {
21        System.out.println("子类的sub方法");
22    }
23
24    public static void main(String[] args) {
25        BaseClass ploymophicBc = new SubClass();
26        System.out.println(ploymophicBc.book);
27        ploymophicBc.base();
28        ploymophicBc.test();
29        // 因为 ploymophicBc 的编译时类型是 BaseClass
30        // BaseClass 类没有提供 sub 方法,所以下面代码编译时会出错
31        // ploymophicBc.sub();
32    }
33}

上面的例子中,引用变量 ploymophicBc 比较特殊,它的编译时类型是 BaseClass,而运行时类型是 SubClass。

ploymophicBc.sub() 这行代码会在编译时报错,因为 ploymophicBc 编译时类型为 BaseClass,而 BaseClass 中没有定义 sub 方法,因此编译时无法通过。

但是注意,ploymophicBc.book 的值为 6, 而不是 ”轻量级 Java EE“。因为对象的实例变量不具备多态性,系统总是试图访问它编译时类型所定义的成员变量,而非运行时。

子类其实是一种特殊的父类,因此 java 允许把父类的引用指向子类对象,这被称为向上转型(upcasting),向上转型由系统自动完成。

可以调用哪些方法,取决于引用类型(编译时)。
具体调用哪个方法,取决于引用指向的实例对象(运行时)。

向下类型转换

问题:引用变量在代码编译过程中,只能调用它编译时类型具备的方法,而不能调用它运行时类型具备的方法

解决:强制转换成运行时类型

方法:引用类型之间的转换只能在有继承关系的两个类型之间进行,否则编译出错。如果想把一个父类引用变量的编译时类型转换成子类类型,则这个引用变量的运行时类型得是子类类型,否则引发 ClassCastException

示例代码:

1//创建子类对象  
2Dog dog = new Dog();  
3
4// 向上类型转换(类型自动提升),不存在风险  
5Animal animal = dog;  
6
7// 风险演示 animal 指向 Dog 类型对象,没有办法转化成 Cat 对象,编译阶段不会报错,但是运行会报错
8Cat cat = (Cat)animal; // 1.编译时按 Cat 类型  2. 运行时 Dog 类型,类型不匹配,直接报错  

instanceof

为了解决强制类型转换,可能引发的 ClassCastException 异常,引入 instanceof 运算符。

instanceof 运算符的含义:用于判断左边的对象(运行时类型或者叫实际类型)是否是右边的类或者其子类、实现类的实例。如果是返回 true,否则返回 false。

在之前的代码中,强制类型转换前使用 instanceof 判断:

1if (anmial instanceof Cat) {
2    Cat cat = (Cat)animal;
3}

final 修饰符

final 修饰类

不能被继承

final 修饰方法

不可被子类覆盖

final 修饰变量

特征:变量一旦被初始化,便不可改变

初始化:定义时直接赋值、借助构造函数

对于基本类型域而言,其值是不可变的。

对于引用类型变量而言,它保存的仅仅只是个引用。final 只保证这个变量所引用的地址不会改变,即一直引用同一个对象。但这个对象自身内容完全可以发生改变。

Object 类

toString

toString 用于输出对象的自我描述信息。

Object 类提供的 toString 返回该对象实现类的 "类名 + @ + hashCode"。通常需要重写该方法。

==

对于数值类型的基本变量,只要两个变量的值相等(不需要数据类型完全相同),就返回 true。

对于两个引用类型的变量,只有它们指向同一个对象时,== 判断才会返回 true。

equals

equals 方法是 Object 类提供的一个实例方法。对于引用变量,只有指向同一个对象时才返回 true。一般需要重写 equals 方法。

重写 equals 方法的示例:

 1    public boolean equals(Object obj) {
2        if (this == obj) {
3            return true;
4        }
5        if (obj !=null && obj.getClass() == Person.class) {
6            Person personObj = (Person)obj;
7            if (this.getIdStr().equals(personObj.getIdStr())) {
8                return true;
9            }
10        }
11        return false;
12    }

equals 为 true,hashCode 就应该相等,这是一种约定俗称的规范。即 equals 为 true 是 hashCode 相等的充分非必要条件。

接口

设计思想

  • 接口体现的是规范和实现分离的设计哲学,让软件系统的各组件之间面向接口耦合,是一种松耦合的设计

  • 接口定义的是多个类共同的公共行为规范,这些行为是与外部交流的通道,意味着接口通常是定义一组公共方法

定义

  • 接口的修饰符,只能是 public 或者 default

  • 由于接口定义的是一种规范,所以接口里不能包含构造器和初始化块定义,只能包含静态常量、方法(只能是抽象方法,类方法和默认方法)以及内部类、内部接口、内部枚举

  • 接口里的常量只能是静态常量,默认使用 public static final 修饰

  • 接口里的内部类、内部接口、内部枚举,默认使用 public static 修饰

  • 接口里的抽象方法不能有方法体,但类方法和默认方法必须有方法体。

方法说明

接口中定义抽象方法可以省略 abstract 关键字和修饰符,默认修饰符为 public。

Java 8 新增允许在接口中定义默认方法,使用 default 修饰。默认情况下,系统使用 public 修饰默认方法。

Java 8 新增允许在接口中定义私有方法。

Java 8 新增允许在接口中定义静态方法。静态方法可以被实现的接口的类继承。

使用

一个类可以实现一个或多个接口。

一个类实现一个或多个接口,这个类必须重写所实现的接口中的所有抽象方法。否则,该类必须被定义成抽象类,保留从父接口继承到的抽象方法。

接口不能用来创建实例,但是可以用于声明引用类型的变量,该变量必须指向实现该接口的类的实例对象。

抽象类

抽象类与普通类的区别,可以概括为 “有得有失”。

得,是指抽象类多了一个能力,抽象类可以包含抽象方法

失,是指抽象类失去了一个能力,抽象类不能用于创建实例

抽象类和普通类的区别:

  • 抽象类使用 abstract 修饰

  • 抽象类可以和普通类一样可以包含成员变量、方法、构造器、初始化块、内部类。但抽象类不能被实例化,抽象类的构造器主要用来被子类调用

  • 抽象类可以不包含抽象方法,但是含有抽象方法的类必须被定义为抽象类

抽象类的设计思想:抽象类是模板模式的设计模式体现。抽象类是从多个具体类中抽象出来的父类,具有更高层次的抽象。从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类为其子类的模板,避免子类设计的随意性

内部类

成员内部类

非静态内部类
 1public class Cow {
2    private double weight;
3
4    public Cow() {
5    }
6
7    public Cow(double weight) {
8        this.weight = weight;
9    }
10    // 定义一个非静态内部类
11    private class CowLeg {
12        private double length;
13        private String color;
14
15        public CowLeg() {}
16        public CowLeg(double length, String color) {
17            this.length = length;
18            this.color = color;
19        }
20        public double getLength() {
21            return this.length;
22        }
23        public void setLength(double length) {
24            this.length = length;
25        }
26        public String getColor() {
27            return this.color;
28        }
29        public void setColor(String color) {
30            this.color = color;
31        }
32        public void info() {
33            System.out.println("当前牛腿的颜色是 " + this.color + ", 长 " + this.length);
34            // 直接访问外部类的 private 修饰的成员变量
35            System.out.println("该牛腿所属的奶牛重: " + weight);
36        }
37    }
38
39    public void test() {
40        CowLeg cl = new CowLeg(1.12"黑白相间");
41        cl.info();
42    }
43    public static void main(String[] args) {
44        Cow cow = new Cow(378.9);
45        cow.test();
46    }
47}

在非静态内部类里可以直接访问外部类的 private 成员,这是因为在非静态内部类对象里,保存了一个它所寄生的外部类对象的引用。如下图:

Java 基础篇专栏 - 类与对象

如果外部类成员变量、内部类成员变量与内部类里方法的局部变量名同名

  • 直接访问局部变量

  • this,访问内部类实例的变量

  • 外部类类名.this.varName 访问外部类实例变量

外部类不能直接访问非静态内部类的成员,无论非静态内部类的成员是什么修饰符修饰的。只能显示创建非静态内部类对象来访问其实例成员。

静态内部类(类内部类)

如果用 static 修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此也叫做类内部类。即静态内部类是外部类的一个静态成员。

静态内部类可以包含静态成员,也可以包含非静态成员。

静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。

外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。

在外部类之外访问内部类

在外部类以外的地方访问内部类(包括静态和非静态两种),则内部类不能使用 private 修饰,private 修饰的内部类只能在外部类内部使用。对于使用其他访问修饰符的内部类,按照访问修饰符范围访问。

  • 省略访问控制符的内部类,只能被与外部类处于同一个包中的其他类所访问

  • 使用 protected 修饰的内部类,可被与外部类处于同一个包中的其他类和外部类的子类访问

  • 使用 public 修饰符的内部类,可在任何地方被访问

在外部类之外使用非静态内部类

由于非静态内部类的对象必须寄生在外部类的对象里,因此在创建非静态内部类对象之前,必须先创建其外部类对象。

示例代码,如下:

1public class Out {
2    // 使用默认访问控制符,同一个包中的其他类可以访问该内部类
3    class In {
4        public In(String msg) {
5            System.out.println(msg);
6        }
7    }
8}
 1public class CreateInnerInstance {
2    public static void main(String[] args) {
3        Out.In in = new Out().new In("Test Msg");
4        /*
5        上面代码可以改为如下三行代码
6        使用 OutterClass.InnerClass 的形式定义内部类变量
7        Out.In in;
8        创建外部类实例,非静态内部类实例将寄生在该实例中
9        Out out = new Out();
10        通过外部类实例和new来调用内部类构造器创建非静态内部类实例
11        in = out.new In("Test Msg");
12        */

13    }
14}

下面定义了一个子类继承了 Out 类的非静态内部类 In 类

1public class SubClass extends Out.In{
2    // 显示定义 SubClass 的构造器
3    public SubClass(Out out){
4        out.super("hello");
5    }
6}

上面的代码可能看起来很怪,其实很正常:非静态内部类 In 类的构造器必须使用外部类对象来调用,代码中 super 代表调用 In 类的构造器,而 out 则代表外部类对象。

如果需要创建 SubClass 对象时,必须创建一个 Out 对象。因为 SubClass 是非静态内部类 In 的子类,非静态内部类 In 对象里必须有一个对 Out 对象的引用,其子类 SubClass 对象里也应该持有对 Out 对象的引用。当创建 SubClass 对象时传给该构造器的 Out 对象,就是 SubClass 对象里 Out 对应引用所指向的对象。

结合上面两段代码,非静态内部类 In 对象和 SubClass 对象都必须持有指向 Outer 对象的引用,区别是创建两种对象时传入 Out 对象的方式不同:当创建非静态内部类 In 类的对象时,必须通过 Outer 对象来调用 new 关键字;当创建 SubClass 类的对象时,必须使用 Outer 对象作为调用者来调用 In 类的构造器

在外部类之外使用静态内部类

因为静态内部类是外部类类相关的,因此创建静态内部类对象时无需创建外部类对象。

 1public class CreateStaticInnerInstance {
2    public static void main(String[] args) {
3        StaticOut.StaticIn in = new StaticOut.StaticIn();
4        /* 上面的代码可改为如下两行代码
5        使用 OuterClass.InnerClass 的形式定义内部类变量
6        StaticOut.StaticIn in;
7        通过 new 调用内部类构造器创建静态内部类实例
8        in = new StaticOut.StaticIn();
9        */

10    }
11}

因为调用静态内部类的构造器时不需要使用外部类对象,所以创建静态内部类的子类也比较简单。下面代码为静态静态内部类 StaticIn 定义了一个空的子类

1public class StaticSubClass extends StaticOut.StaticIn {}

局部内部类

匿名内部类

匿名内部类适合创建只需要一次使用的类,创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名类不能重复使用。

匿名类是用来创建接口或者抽象类的实例的。

匿名内部类不能定义构造器。因为匿名内部类没有类名,所有无法定义构造器。但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。

定义匿名内部类格式如下:

1new 实现接口 | 抽象父类构造器(实参列表)
2{
3    匿名内部类的类体部分
4}

最常用的创建匿名内部类的方式是需要创建某个接口类型的对象,如下

1public interface ProductA {
2    public double getPrice();
3    public String getName();
4}
 1public class AnonymousTest {
2    public void test(ProductA p) {
3        System.out.println("Buy a" + p.getName() + "Cost " + p.getPrice());
4    }
5    public static void main(String[] args) {
6        AnonymousTest ta = new AnonymousTest();
7        // 调用 test() 方法时,需要传入一个 Product 参数
8        // 此处传入其匿名实现类的实例
9        ta.test(new ProductA() {
10            @Override
11            public double getPrice() {
12                return 567.8;
13            }
14
15            @Override
16            public String getName() {
17                return "APG Card";
18            }
19        });
20    }
21}

通过继承抽象父类来创建匿名内部类时,匿名内部类将拥有和父类相同形参列表的构造器。看下面一段代码

 1public abstract class Device {
2    private String name;
3    public abstract double getPrice();
4    public Device() {};
5    public Device(String name) {
6        this.name = name;
7    }
8    public String getName() {
9        return this.name;
10    }
11    public void setName(String name) {
12        this.name = name;
13    }
14}
 1public class AnonymousInner {
2    public void test(Device d) {
3        System.out.println("Buy a" + d.getName()+ "Cost" + d.getPrice());
4    }
5    public static void main(String[] args) {
6        AnonymousInner ai = new AnonymousInner();
7        // 调用有参数的构造器创建 Device 匿名实现类的对象
8        ai.test(new Device("电子显示器") {
9            @Override
10            public double getPrice() {
11                return 67.8;
12            }
13        });
14
15        // 调用无参数的构造器创建 Device 匿名实现类的对象
16        Device d = new Device() {
17            // 初始化块
18            {
19                System.out.println("匿名内部类的初始化块");
20            }
21            // 实现抽象方法
22            @Override
23            public double getPrice() {
24                return 56.2;
25            }
26            // 重写父类的实例方法
27            public String getName() {
28                return "keyboard";
29            }
30        };
31        ai.test(d);
32    }
33}


Java 基础篇专栏 - 类与对象

原文始发于微信公众号(SRE工程师):Java 基础篇专栏 - 类与对象