我上大学的时候不爱学习,爱打游戏,每次都临考试前一个月疯狂抱佛脚,抱得佛估计都怕了,印象最深的是我大二学期的 Java 课考了 60 分,别看我及格了但也是极其危险,笔试前又去抱佛的脚,结果答得一般般,上机考试就更别提了,瞎写了几行,编译器都快看不下去了... 估计能考及格也是老师高抬贵手,当时找过系里老师两三次,就差提果篮去老师家上门了。
有时候经常会想,假如时光能倒流,让自己教大学时期学不进去的自己计算机、编程这类的知识,自己能不能学进去,考试的时候还会不会挂科,是老师当时照本宣科根本吸引不了我,还是我当时就没心思学习。
于是我特地总结了下当时大学觉得最难的面向对象的知识,由于这部分知识实在太多我分成了基础和进阶篇,我确认大学教的时候没这么多,这次先上基础篇,反射、各种内部类、抽象类、枚举、接口什么的下期再讲,大纲如下:
类是用来描述同一类事物的,可以在类中定义任意数量的、不同类型的变量,作为这一类事物的属性。这种属性叫做成员变量(member variable),除了类的属性外,类还可以有一组描述自己行为的方法(method)。其实类就相当于我们自己定义了一种数据类型。
比如下面我们定义了一个代表商品的类:
- public class Commodity {
-
- String name;
- String id;
- int count;
- double price;
-
- }
-
public class代表这个类是公共类(公共类名必须和文件名相同),商品类里定义了四个成员变量用来代表商品的四个属性--商品名、商品标识、商品数量和商品价格。
类的定义,其实就是创建了一个模版。描述了一种我们需要的数据类型。
从数据类型的角度看,类就是我们根据自己的需要,创建了一种新的数据类型。所以类也叫做“自定义类型”。 一个 Java 程序中不允许类同名。
而对象(object)是类这个“自定义类型”的具体实例(instance)。
后面可能有时候会说对象、有时候会说类的实例,记住咱们要表述的其实是同一个东西,不必太刻意区分。
在 Java 中可以使用 new 操作符创建一个类的对象。在使用 new 创建的对象中,类中定义的那些成员变量都会被赋以其类型的初始值。看下面这个使用类对象的例子:
- public class UserClassCase {
- public static void main(String[] args) {
-
- // 创建一个Commodity类的实例,用变量m1指向它。
- Commodity m1 = new Commodity();
- // 使用点操作符,给m1指向的实例赋值。
- m1.name = "茉莉花茶包 20 包";
- m1.id = "000099518";
- m1.count = 1000;
- m1.price = 99.9;
-
- // 创建另外一个Commodity类的实例,用变量m1指向它。
- Commodity m2 = new Commodity();
- m2.name = "可口可乐 330ml";
- m2.id = "000099519";
- m2.count = 1000;
- m2.price = 3.0;
-
- // 卖出一个商品1
- int m1ToSold = 1;
- System.out.println("感谢购买" + m1ToSold + "个" + m1.name + "。商品单价为"
- + m1.price + "。消费总价为" + m1.price * m1ToSold + "。");
- m1.count -= m1ToSold;
- System.out.println(m1.id + "剩余的库存数量为" + m1.count);
-
- // 卖出3个商品2
- int m2ToSold = 3;
- System.out.println("感谢购买" + m2ToSold + "个" + m2.name + "。商品单价为"
- + m2.price + "。消费总价为" + m2.price * m2ToSold + "。");
- m2.count -= m2ToSold;
- System.out.println(m2.id + "剩余的库存数量为" + m2.count);
-
- }
- }
-
-
引用类型是使用 Java 时经常被提到的一个名词,Java 中的数据类型分为:基本数据类型和引用数据类型。上节说到变量的名和实,有这样一个概念 :
那么引用数据类型和基本数据类型的差异在,基本数据类型变量的值,就内存地址里存放的值,而引用数据类型的变量值还是一个地址,需要跳到“值地址”对应的内存才能找到实例。
上面说的就是引用类型的实质,引用类型是Java的一种内部类型,是对所有自定义类型和数组引用的统称,并非特指某种类型。
下面看个例程加深下理解: 例程中 m1 是一个Commodity 类型的引用,它只能指向 Commodity 类型的实例,引用数据类型的变量包含两部分信息:类型和实例。也就是说,每一个引用数据类型的变量(简称引用),都是指向某个类( class /自定义类型)的一个实例/对象(instance / object)。不同类型的引用在 Java 的世界里都是引用。
引用的类型信息在创建时就已经确定,可以通过给引用赋值,让其指向不同的实例,比如 m1 就是 Commodity 类型的引用,可以通过赋值操作让它指向不同的 Commodity 类型实例。这个其实很好理解,就跟把一个整型的值赋值给浮点型的变量在 Java 里是不被允许的一样,基础类型也是相同类型的值才能对变量进行赋值操作。
- public class ReferenceAndPrimaryDataType {
- public static void main(String[] args) {
- Commodity m1;
- m1 = new Commodity();
- Commodity m2 = new Commodity();
- Commodity m3 = new Commodity();
- Commodity m4 = new Commodity();
- Commodity m5 = new Commodity();
-
- // 给一个引用赋值,则两者的类型必须一样。m5可以给m1赋值,因为他们类型是一样的
- m1 = m5;
-
- System.out.println("m1=" + m1);
- System.out.println("m2=" + m2);
- System.out.println("m3=" + m3);
- System.out.println("m4=" + m4);
- System.out.println("m5=" + m5);
-
- Commodity m6 = m1;
- System.out.println("m6=" + m6);
- m6 = m5;
- System.out.println("m6=" + m6);
-
- System.out.println("m1=" + m1);
- System.out.println("m2=" + m2);
- System.out.println("m3=" + m3);
- System.out.println("m4=" + m4);
- System.out.println("m5=" + m5);
-
- int a = 999;
-
- }
- }
-
-
- Commodity m1 = new Commodity();
-
下面通过例程加深一下理解
- public class Commodity {
- String name;
- String id;
- int count;
- double price;
-
- }
-
- public class Commodity1 {
- String name;
- String id;
- int count;
- double price;
-
- }
-
- public class ClassInstanceAndRef {
- public static void main(String[] args) {
- Commodity m = new Commodity();
- Commodity1 m1 = new Commodity1();
-
- Commodity commodity = m;
- // 即使 Commodity 和 Commodity1 的内容一摸一样,那也是不同的类。
- // 不同类的引用不可以互相赋值,因为它们本质上是不同的对象。
- Commodity commodity = m1;
- }
- }
-
null 是引用类型的缺省值,null 代表空,不存在,也常被成为空指针,因为它不指向任何已存的实例。引用类型的数组创建出来,每个元素的初始值就都是null。
Java里比较常见的错误 NullPointerException 就是因为 null 带来的问题,看一下下面这个例程,Commodity 类型的数组,因为 Commodity 是引用类型,数组创建后默认元素值是 null, 接下来我们选择性的给数组元素进行赋值,然后当做每个元素都已经被赋值 Commodity 对象一样,在循环里调用 Commodity 对象的属性,看看会发生什么问题。
- public class RefAndNull {
- public static void main(String[] args) {
- // 数组在创建出来之后,会按照类型给数组中的每个元素赋缺省值。
- // 引用类型的缺省值是null
- Commodity[] ms = new Commodity[9];
- // 给索引为偶数的元素赋值
- for (int i = 0; i < ms.length; i++) {
- if (i % 2 == 0) {
- ms[i] = new Commodity();
- }
- }
- // 依次输出数组的值
- for (int i = 0; i < ms.length; i++) {
- System.out.println(ms[i]);
- }
-
- for (int i = 0; i < ms.length; i++) {
- Commodity m = ms[i];
- System.out.println(m.price);
- System.out.println(m.count);
- System.out.println(m.name);
- System.out.println(m.id);
- }
- }
- }
-
上面这个程序,当打印实例属性的循环执行到数组的奇数索引元素的时候就会出现运行时错误 NullPointerException,程序中断。
- Exception in thread "main" java.lang.NullPointerException
- at RefAndNull.main(RefAndNull.java:21)
-
这个问题,只要足够细心就能避免,在使用引用之前一定要检查一下它是不是 null,所以上面的例程加一个判断就能正常运行。
- if (m[i] != null) {
- System.out.println(m.price);
- System.out.println(m.count);
- System.out.println(m.name);
- System.out.println(m.id);
- }
-
上面咱们说过了,Java 程序中,不允许类名重复,其实这里说的类名指的的类全限定名,不是我们看到的简单类名,如果是的话也太容易重复了,什么是全限定名呢,就是加上包名的类名。
为了避免类太多,放在在一起混乱,可以把类放在文件夹里。这时就需要使用 package 语句告诉 Java 这个类在哪个 package 里。 package 语句要和源文件的目录完全对应,大小写也要一致。 我们常把 package 称作包。一般来说,类都会在包里,而不会直接放在项目的根目录。
看一下下面这个例子:
- package com.phone.parts;
-
- public class Mainboard {
- public CPU cpu;
- public Memory memory;
- public Storage storage;
- public String model;
- // 上市年份
- public int year;
- }
-
当使用另一个包里的类的时候,需要使用类的全限定名,即要带上包名。比如我们在com.phone包下有一个 phone 类,想要使用上面定义的com.phone.parts包里的Mainboard类,那么就必须像下面这样:
- package com.phone;
-
- public class Phone {
- com.phone.parts.Mainboard mainboard;
- }
-
如果每次都使用带包名的类就太繁琐了,这个时候就可以在类的上面使用 import 语句,把类导入使用,就可以省略掉包名。
- package com.phone;
-
- import com.phone.parts.Mainboard;
-
- public class Phone {
- Mainboard mainboard;
- }
-
如果要导入多个类,则需要使用多个 import 语句
- package com.phone;
-
- import com.phone.parts.Mainboard;
- import com.phone.parts.CPU;
-
- public class Phone {
- Mainboard mainboard;
- CPU cpu;
-
- }
-
如果需要导入一个包的多个类,可以使用 * 通配符,它会导入包目录下的所有类
- package com.phone;
-
- import com.phone.parts.*;
-
- public class Phone {
- Mainboard mainboard;
- CPU cpu;
-
- }
-
- package com.phone.parts;
-
- public class Mainboard {
- // 缺省访问修饰符,所以cpu属性只能在包内部访问
- CPU cpu;
- public Memory memory;
- public Storage storage;
- public String model;
- // 上市年份
- public int year;
- }
-
- ---
-
- package com.phone;
-
- public class TestUseMainboard {
- public static void main(String[] args) {
- Mainboard mainboard = new Mainboard();
- // 这里会编译报错,在com.phone包里访问不到mainboard的cpu属性
- mainboard.cpu = new CPU();
- mainboard.cpu.producer="aaa";
- }
- }
-
面向对象里,方法代表的是类的行为,是它们的动态描述(而属性是静态描述),比如下面的商品类进行商品描述的方法 describe,计算商品利率的 calculateProfit,获取商品数量的 getCurrentCount 都是方法。
方法的构成形式是: 【访问控制符 返回值类型 方法名 方法参数列表 方法体 】这几个部分构成,结合下面几个方法的例子会更容易理解,每个关键部分都用注释做了说明。
- public class CommodityV2 {
-
- public String name;
- public String id;
- public int count;
- public double soldPrice;
- public double purchasePrice;
- String madeIn;
-
- // >> 访问修饰符 public/protected/private 当然也可以缺省
- // >> 返回值类型:无需返回值则用void表示,void是Java中的关键字
- // >> 方法名:任意合法的标识符都可以
- // >> 参数列表:后续讲解
- // >> 方法体:方法的代码
- // >> 方法体内部定义的变量叫做局部变量
- public void describe() {
- double netIncome = soldPrice - purchasePrice;
- System.out.println("商品名字叫做" + name + ",id是" + id + "。 商品售价是" + soldPrice
- + "。商品进价是" + purchasePrice + "。商品库存量是" + count +
- "。销售一个的毛利润是" + netIncome + "。制造地为" + madeIn);
- }
-
- // >> 在方法定义中指定方法的返回值类型
- // >> Java中一个方法只能有一种返回值,如果不需要返回值则用void表示
- // >> 如果定义了返回值,则必须使用 return 语句返回方法的返回值,return 是 Java 的关键字
- // >> 可以认为,返回值必须要能够用来给返回值类型的变量赋值
- public double calculateProfit(){
- double profit = soldPrice - purchasePrice;
- // >> 这个return是代码块里的return,是return所在代码块的最后一个语句
- if (profit <= 0) {
- return 0;
- }
- // >> return 语句必须是所在代码块的最后一个语句,否则就是语法错误
- return profit;
-
- // >> 一个方法可以有多个返回语句,但只能有一个生效。
- }
-
- // >> 返回值如果是基本类型,则要类型完全相同,或者符合类型自动转换规则
- public double getCurrentCount(){
- return count;
- }
-
- // >> 如果不符合规则,可以使用强制类型转换
- public int getIntSoldPrice(){
- return (int) soldPrice;
- }
- }
-
方法里隐藏着一个this自引用,指向调用这个方法的对象。使用一个对象调用方法,也叫做在这个对象上调用方法,因为方法可以访问这个对象的值。访问一个成员变量的完整形态,是"this.成员变量的名字" , 这个 this 是可以省略的,方法访问成员变量的时候默认就是访问的当前调用方法的对象。 下面两个方法 addCount 和 addCountWithoutThis 都是正确的,且效果一致。
- public class CommodityV2 {
-
- ...
- public int count;
-
- public void addCount(int count) {
- this.count += count;
- }
-
- public void addCountWithoutThis(int count) {
- count += count;
- }
- }
-
- public class CommodityWithOverload {
-
- public String name;
- public String id;
- public int count;
- public double soldPrice;
- public double purchasePrice;
-
- public void init(String name, String id, int count, double soldPrice, double purchasePrice) {
- this.name = name;
- this.id = id;
- this.count = count;
- this.soldPrice = soldPrice;
- this.purchasePrice = purchasePrice;
- }
-
- public void describe() {
- System.out.println("商品名字叫做" + name + ",id是" + id + "。 商品售价是" + soldPrice
- + "。商品进价是" + purchasePrice + "。商品库存量是" + count +
- "。销售一个的毛利润是" + (soldPrice - purchasePrice));
- }
-
- // >> TODO 重载的方法可以调用别的重载方法,当然也可以调用别的不重载的方法。
- // >> TODO 实际上,像这种补充一些缺省的参数值,然后调用重载的方法,是重载的一个重要的使用场景。
- // >> TODO 在这里我们举的例子就是这样的,但是不是语法要求一定要这样。重载的方法的方法体内代码
- // TODO 可以随便写,可以不调用别的重载方法
- public double buy() {
- return buy(1);
- }
-
- public double buy(int count) {
- return buy(count, false);
- }
-
- // TODO 最后都补充好参数,调用参数最全的一个方法
- public double buy(int count, boolean isVIP) {
- if (this.count < count) {
- return -1;
- }
- this.count -= count;
- double totalCost = count * soldPrice;
- if (isVIP) {
- return totalCost * 0.95;
- } else {
- return totalCost;
- }
- }
-
- }
-
重载的方法可以调用别的重载方法,实际上,像这种补充一些缺省的参数值,然后调用重载的方法,一直调用到最后都补充好参数,调用参数最全的那个重载方法,是重载的一个重要的使用场景。
但是,不是语法要求一定要这样。重载的方法的方法体内代码可以随便写,可以不调用别的重载方法。
- public class CommodityWithConstructor {
-
- public String name;
- public String id;
- public int count;
- public double soldPrice;
- public double purchasePrice;
-
- public CommodityWithConstructor(String name, String id, int count, double soldPrice, double purchasePrice) {
- this.name = name;
- this.id = id;
- this.count = count;
- this.soldPrice = soldPrice;
- this.purchasePrice = purchasePrice;
- }
- }
-
- CommodityWithConstructor m = new CommodityWithConstructor("书桌", "DESK9527", 40, 999.9, 500);
-
与普通方法一样,也能给类的构造方法定义重载方法,在构造方法里才能调用重载的构造方法。语法为: this(实参列表)。
- public class CommodityV3 {
-
- public String name;
- public String id;
- // >> TODO 构造方法执行前,会执行给局部变量赋初始值的操作
- // >> TODO 我们说过,所有的代码都必须在方法里,那么这种给成员变赋初始值的代码在哪个方法里?怎么看不到呢?
- // TODO 原来构造方法在内部变成了
方法。学习就是要脑洞大,敢想敢试,刨根问底。 - public int count = 999;// 999/0;
- public double soldPrice;
- public double purchasePrice;
-
- public CommodityV3(String name, String id, int count, double soldPrice, double purchasePrice) {
- this.name = name;
- this.id = id;
- this.count = count;
- this.soldPrice = soldPrice;
- this.purchasePrice = purchasePrice;
- soldPrice = 9/0;
- }
-
- // 构造函数体内调用构造函数的重载方法时,调用语句必须是第一行
- public CommodityV3(String name, String id, int count, double soldPrice) {
- this(name, id, count, soldPrice, soldPrice * 0.8);
- // double purPrice = soldPrice * 0.8;
- }
-
- public CommodityV3() {
- this("无名", "000", 0, 1, 1.1);
-
- }
- }
-
- public class CommodityV3 {
- ......
- public CommodityV3() {
- this("无名", "000", 0, 1, 1.1);
- }
- }
- ------
- public class useCommodityV3App {
- public static void main(String[] args) {
- CommodityV3 m3 = new CommodityV3()
- }
- }
-
就是构造函数重载的一个常用的使用场景。
下面的类定义里除了普通的成员变量还增加了静态变量。
- public class CommodityWithStatic {
-
- public String name;
- public String id;
- public int count;
- public double soldPrice;
- public double purchasePrice;
-
- public static double DISCOUNT_FOR_VIP = 0.95;
- static int STATIC_VARIABLE_CURR_PACKAGE_ONLY = 100;
-
- public CommodityWithStatic(String name, String id, int count, double soldPrice, double purchasePrice) {
- this.name = name;
- this.id = id;
- this.count = count;
- this.soldPrice = soldPrice;
- this.purchasePrice = purchasePrice;
- }
- }
-
- public class UseStaticVariableApp {
- public static void main(String[] args) {
- // 使用别的类的静态变量的时候,需要使用完整形态:类名.静态变量名字
- CommodityWithStatic.DISCOUNT_FOR_VIP = 0.5;
-
- System.out.println("VIP的折扣是 " + CommodityWithStatic.DISCOUNT_FOR_VIP);
-
- }
- }
-
类除了静态变量外,还有静态方法。
- public class CommodityWithStatic {
-
- public String name;
- public String id;
- public int count;
- public double soldPrice;
- public double purchasePrice;
-
- public static double DISCOUNT_FOR_VIP = 0.95;
- static int STATIC_VARIABLE_CURR_PACKAGE_ONLY = 100;
-
- public static double getVIPDiscount() {
- // 静态方法可以访问静态变量,包括自己类的静态变量和访问控制符允许的别的类的静态变量
- return DISCOUNT_FOR_VIP;
- }
-
- public CommodityWithStatic(String name, String id, int count, double soldPrice, double purchasePrice) {
- this.name = name;
- this.id = id;
- this.count = count;
- this.soldPrice = soldPrice;
- this.purchasePrice = purchasePrice;
- }
- }
-
静态方法通过类名调用
- public class UseStaticMethodeApp {
- public static void main(String[] args) {
- double vipDiscount = CommodityWithStatic.getVIPDiscount()
-
- System.out.println("VIP的折扣是 " + vipDiscount);
-
- }
- }
-
静态方法的访问和静态变量一样,通过类名访问,不过当前类在访问自己类里的静态方法和变量时可以省略类名
- public class CommodityWithStatic {
-
- public String name;
- public String id;
- public int count;
- public double soldPrice;
- public double purchasePrice;
-
- public static double DISCOUNT_FOR_VIP = 0.95;
- static int STATIC_VARIABLE_CURR_PACKAGE_ONLY = 100;
-
- public static double getVIPDiscount() {
- // 静态方法可以访问静态变量,包括自己类的静态变量和访问控制符允许的别的类的静态变量
- return DISCOUNT_FOR_VIP;
- }
-
- public double buy(int count, boolean isVIP) {
- if (this.count < count) {
- return -1;
- }
- this.count -= count;
- double totalCost = count * soldPrice;
- if (isVIP) {
- // 静态方法的访问和静态变量一样,可以带上类名,当前类可以省略类名
- return totalCost * getVIPDiscount();
- } else {
- return totalCost;
- }
- }
- }
除了上面看到的静态变量和方法外,Java里还有一个东西叫静态代码块。见下面的例程
- public class DiscountMgr {
-
- public static void main(String[] args) {
- System.out.println("最终main 方法中使用的SVIP_DISCOUNT是" + SVIP_DISCOUNT);
- }
-
- public static double BASE_DISCOUNT;
-
- public static double VIP_DISCOUNT;
-
- public static double SVIP_DISCOUNT;
-
- static {
- BASE_DISCOUNT = 0.99;
- VIP_DISCOUNT = 0.85;
- SVIP_DISCOUNT = 0.75;
-
- // 静态代码块里可以有任意的合法代码
- System.out.println("静态代码块1里的SVIP_DISCOUNT" + SVIP_DISCOUNT);
-
- }
-
- static {
- SVIP_DISCOUNT = 0.1;
- System.out.println("静态代码块2里的SVIP_DISCOUNT" + SVIP_DISCOUNT);
- }
-
- }
-
public,全局可见,在包外也可以使用。
protected,在本类、子类内可见(子类在其他包也可见,这点与缺省控制符不同)。
缺省,在当前包可见,对外部包不可见。
private,只在当前类内可见。
类内部 | 本包 | 子类 | 外部包 |
---|---|---|---|
private | |||
default | default | ||
protected | protected | protected | |
public | public | public | public |
- package com.example.factory;
-
- // 类,静态方法,静态变量,成员变量,构造方法,成员方法都可以使用访问修饰符
- public class Commodity {
-
- private String name;
- private String id;
- private int count;
- private double soldPrice;
- private double purchasePrice;
- private NonPublicClassCanUseAnyName nonPublicClassCanUseAnyName;
- public static double DISCOUNT = 0.1;
-
- // 构造方法如果是private的,那么就只有当前的类可以调用这个构造方法
- public Commodity(String name, String id, int count, double soldPrice, double purchasePrice) {
- this.name = name;
- this.id = id;
- this.count = count;
- this.soldPrice = soldPrice;
- this.purchasePrice = purchasePrice;
- }
-
- // 有些时候,会把所有的构造方法都定义成private的,然后使用静态方法调用构造方法
- // 同样的,这样的好处是可以通过代码,检查每个属性值是否合法。
- public static Commodity createCommodity(String name, String id, int count,
- double soldPrice, double purchasePrice) {
- if (soldPrice < 0 || purchasePrice < 0) {
- return null;
- }
- return new Commodity(name, id, count, soldPrice, purchasePrice);
- }
-
- public Commodity(String name, String id, int count, double soldPrice) {
- this(name, id, count, soldPrice, soldPrice * 0.8);
- }
-
- // public的方法类似一种约定,既然外面的代码可以使用,就意味着不能乱改。比如签名不能改之类的
- public void describe() {
- System.out.println("商品名字叫做" + name + ",id是" + id + "。 商品售价是" + soldPrice
- + "。商品进价是" + purchasePrice + "。商品库存量是" + count +
- "。销售一个的毛利润是" + (soldPrice - purchasePrice));
- freeStyle();
- }
-
- // 对于private的方法,因为类外面掉不到,所以无论怎么改,也不会影响(直接影响)类外面的代码
- private void freeStyle() {
-
- }
-
- public double calculateProfit() {
- double profit = soldPrice - purchasePrice;
- return profit;
- }
-
- public double buy(int count) {
- if (this.count < count) {
- return -1;
- }
- return this.count -= count;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public String getId() {
- return id;
- }
-
- public void setId(String id) {
- this.id = id;
- }
-
- public int getCount() {
- return count;
- }
-
- public void setCount(int count) {
- this.count = count;
- }
-
- public double getSoldPrice() {
- return soldPrice;
- }
-
- public void setSoldPrice(double soldPrice) {
- this.soldPrice = soldPrice;
- }
-
- public double getPurchasePrice() {
- return purchasePrice;
- }
-
- public void setPurchasePrice(double purchasePrice) {
- this.purchasePrice = purchasePrice;
- }
- }
上面的Commodity 类里还用到了同包下的 NonPublicClassCanUseAnyName 类
- package com.example.factory;
-
- class NonPublicClassCanUseAnyName {
- }
起这个类型也是为了表示,非public的类,类名可以使用任意名字,不用必须和文件名相同,不过这个类它也就只能在包内被使用了。
上面成员变量的 getter 和 setter 非常多,一般我们不用自己定义,lombok 包提供了我们一些注解,能自动给类加上getter、setter、构造方法等。这个后面用到了再去学。
上边一直在用的 Commodity 类是对商品的抽象,但是它只包含了商品最基本的属性和方法,比如商品名,价格,库存数、进价和售价这些属性。但是商品是可以分门别类划分成很多品类,每一个品类又都有自己的通用属性和方法。
比如手机也是一个商品,但它又是个更细分的品类,除了商品的名称、库存数等商品的通用属性外,它还有自己这个品类独有的属性和方法,比如CPU,内存,品牌,操作系统,屏幕大小等描述信息。那么如果每次有个新商品品类,都把Commodity里定义的内容拷贝在细分的商品类里,就得不偿失了。这个时候就需要类的继承了。
看一下下面对手机类的定义
- package com.example.factory;
-
- public class Phone extends Commodity {
-
- // 给Phone类增加新的属性和方法
- private double screenSize;
- private double cpuHZ;
- private int memoryG;
- private int storageG;
- private String brand;
- private String os;
-
- public Phone(
- String name, String id, int count, double soldPrice, double purchasePrice,
- double screenSize, double cpuHZ, int memoryG, int storageG, String brand, String os
- ) {
-
- this.screenSize = screenSize;
- this.cpuHZ = cpuHZ;
- this.memoryG = memoryG;
- this.storageG = storageG;
- this.brand = brand;
- this.os = os;
-
- this.setName(name);
- this.setId(id);
- this.setCount(count);
- this.setSoldPrice(soldPrice);
- this.setPurchasePrice(purchasePrice);
- }
-
- public void describePhone() {
- System.out.println("此手机商品属性如下");
- describe();
- System.out.println("手机厂商为" + brand + ";系统为" + os + ";硬件配置如下:\n" +
- "屏幕:" + screenSize + "寸\n" +
- "cpu主频" + cpuHZ + " GHz\n" +
- "内存" + memoryG + "Gb\n" +
- "存储空间" + storageG + "Gb\n");
-
- }
- ...... // 省略了属性的Getter 和 Setter
-
- }
-
看下面的例程
- package com.example;
-
- import com.example.factory.Phone;
-
- public class UsePhoneAppMain {
- public static void main(String[] args) {
- Phone phone = new Phone(
- "手机001","Phone001",100, 1999, 999,
- 4.5,3.5,4,128,"索尼","安卓"
- );
- // 调用了父类的describe方法
- phone.describe();
- // 调用了子类的describePhone方法
- phone.describePhone();
- }
- }
-
- package com.example.facotry;
-
- public class Phone extends Commodity {
-
- // 给Phone增加新的属性和方法
- private double screenSize;
- private double cpuHZ;
- private int memoryG;
- private int storageG;
- private String brand;
- private String os;
- private static int MAX_BUY_ONE_ORDER = 5;
-
- public Phone(
- String name, String id, int count, double soldPrice, double purchasePrice,
- double screenSize, double cpuHZ, int memoryG, int storageG, String brand, String os
- ) {
-
- ...... // 跟上面的Commodity内容一样
- }
-
- public double buy(int count) {
-
- // TODO 这个方法里代码大部分和父类一样,肯定有方法解决
- if (count > MAX_BUY_ONE_ORDER) {
- System.out.println("购买失败,手机一次最多只能买" + MAX_BUY_ONE_ORDER + "个");
- return -2;
- }
- if (this.count < count) {
- System.out.println("购买失败,库存不够");
- return -1;
- }
- this.count -= count;
- double cost = count * soldPrice;
- System.out.println("购买成功,花费为" + cost);
- return cost;
- }
-
- // 返回值必须一样,不是类型兼容,而是必须一摸一样。
- // 如果签名一样,但是返回值不一样,会是错误
- // public int buy(int count) {
- // if (count > MAX_BUY_ONE_ORDER) {
- // return -2;
- // }
- // if (this.count < count) {
- // return -1;
- // }
- // return this.count -= count;
- // }
-
- // 返回值必须一样,不是类型兼容,而是必须一摸一样
- // public boolean buy(int count) {
- // return true;
- // }
-
- public String getName() {
- return this.brand + ":" + this.os + ":" + name;
- }
-
- public void describePhone() {
- System.out.println("此手机商品属性如下");
- describe();
- System.out.println("手机厂商为" + brand + ";系统为" + os + ";硬件配置如下:\n" +
- "屏幕:" + screenSize + "寸\n" +
- "cpu主频" + cpuHZ + " GHz\n" +
- "内存" + memoryG + "Gb\n" +
- "存储空间" + storageG + "Gb");
- }
-
- ...... // 省略了属性的Getter 和 Setter
- }
-
例程里的 buy 方法,在父类Commodity里也有,通过使用和父类方法签名一样,而且返回值也必须一样的方法,可以让子类覆盖(override)掉父类的方法。也就是说,子类并不是只能把父类的方法拿过来,而且可以通过覆盖来替换其中不适合子类的方法。
覆盖父类的方法时,方法的签名和返回值都必须一样(返回值类型必须一模一样,不能是兼容模型),如果签名一样但是返回值不一样,程序会报错,因为 Java 是认为定义了两个签名重复的方法(同一个类里方法签名不能重复)。
覆盖可以覆盖掉父类的方法。同一个方法,不同的行为。这就是多态!方法可以覆盖,而属性访问不可以,所以这也是属性访问推荐使用 Getter 和 Setter 方法的一个原因。即使在父类里,只是一个简单的 getName 方法读取name的值,但是这样做,子类就可以覆盖掉父类的方法,方法不止眼前的代码,还有子类的覆盖。所以,用方法,才能覆盖,才能实现面向对象的多态。
注意:子类覆盖父类的方法,不可以用可见性更低的修饰符,但是可以用更高的修饰符
使用super可以调用父类的 public 方法和属性,当然因为子类能继承父类的 public 属性,所以一般 super 只用来在子类方法里调用父类的方法,最常见的还是在子类的方法里再去用 super 调用父类中被子类覆盖的方法,比如:
- package com.example.facotry;
-
- public class Phone extends Commodity {
-
- private static int MAX_BUY_ONE_ORDER = 5;
- ......
-
- public double buy(int count) {
- if (count > MAX_BUY_ONE_ORDER) {
- System.out.println("购买失败,手机一次最多只能买" + MAX_BUY_ONE_ORDER + "个");
- return -2;
- }
- // 调用父类Commodity的buy方法
- return super.buy(count);
- }
-
- ......
- }
-
上面例程里,子类Phone覆盖了父类 Commodity 的 buy 方法,子类的 buy 方法在检查购买数量是否超限,成立后再去调用父类的 buy 方法完成购买功能。这也是方法覆盖比较经典的用法。
除此之外,super 还常用于在子类的构造方法里调用父类的构造方法,我们可以把Phone类的构造方法优化为,调用父类的构造方法初始化通用属性,然后自己再初始化Phone类独有的属性,这样就避免了在子类里再重复写初始化商品通用属性的代码。
- package com.example.factory;
-
- public class Phone extends Commodity {
-
- // 给Phone增加新的属性和方法
- private double screenSize;
- private double cpuHZ;
- private int memoryG;
- private int storageG;
- private String brand;
- private String os;
- private static int MAX_BUY_ONE_ORDER = 5;
-
- public Phone(
- String name, String id, int count, double soldPrice, double purchasePrice,
- double screenSize, double cpuHZ, int memoryG, int storageG, String brand, String os
- ) {
- super(name, id, count, soldPrice * 1.2, purchasePrice);
- init(screenSize, cpuHZ, memoryG, storageG, brand, os);
- }
-
- public void init(double screenSize, double cpuHZ, int memoryG, int storageG, String brand, String os) {
- this.screenSize = screenSize;
- this.cpuHZ = cpuHZ;
- this.memoryG = memoryG;
- this.storageG = storageG;
- this.brand = brand;
- this.os = os;
- }
-
- ......
- }
-
可以用子类的引用给父类的引用赋值,也就是说,父类的引用可以指向子类的对象,但是反之则不行,不能让子类的引用指向父类的对象。因为父类并没有子类的属性和方法。
因为子类继承了父类的方法和属性,所以父类的对象能做到的,子类的对象肯定能做到。换句话说,我们可以在子类的对象上,执行父类的方法。
instanceof 操作符,可以判断一个引用指向的对象是否是某一个类或者其子类的实例,是则返回true,否则返回false。
- package com.example;
-
- import com.example.factory.Phone;
- import com.example.factory.Commodity;
-
- public class InstanceOfTest {
- public static void main(String[] args) {
- Phone phone = new Phone(
- "手机001","Phone001",100, 1999, 999,
- 4.5,3.5,4,128,"索尼","安卓"
- );
- if (phone instanceof Phone) {
- System.out.println("it's an instance of class Phone");
- }
- if (phone instanceof Commodity) {
- System.out.println("it's an instance of class Commodity");
- } else {
- System.out.println("not an instance");
- }
- }
- }
-
- public final class String
- implements java.io.Serializable, Comparable<String>, CharSequence {
- }
-