我上大学的时候不爱学习,爱打游戏,每次都临考试前一个月疯狂抱佛脚,抱得佛估计都怕了,印象最深的是我大二学期的 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();- 使用 new 操作符可以创建某个类的一个实例。在Java程序运行的时候,所有这些创建出来的实例都被Java放在内存一个叫做堆(heap)的地方,堆可以理解为一个类似公告板的东西。
- 创建类的一个实例,就是根据类的定义,点出需要的“纸”(内存区域),加工成一个本子,挂在公告板上。实例本身,可以认为是一个小本子。
- 引用里存放的,相当于某个本子(实例)在布告板上的位置,通过这个位置我们才能定位到这本子(实例),从而访问其中的纸张(属性)。
类,对象和引用的关系
类和对象的关系
- 类相当于对象的模板,对象是类的一个实例。
- 一个 Java 程序中类名相同的类只能有一个,也就是类型不会重名。
- 一个类可以有很多对象。
- 一个对象只能根据一个类(模板)来创建。
引用和类以及对象的关系
- 引用必须是,只能是一个类的引用。
- 引用只能指向其所属的类型的类对象。
- 相同类型的引用之间可以赋值。
- 只能通过指向一个对象的引用,来操作一个对象,比如访问某个成员变量。
下面通过例程加深一下理解
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。
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.NullPointerExceptionat 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中的包和访问控制符
上面咱们说过了,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;
}
- 上面这个Mainboard类文件中,使用 package 声明了包名 com.phone.parts。 所以Mainboard.java 文件必须放在 src/com/phone/parts这个目录里,不能有差异。
- 一个类只能有一个package语句,如果有package语句,则必须是类的第一行有效代码。
- 不同的包里可以有相同名字的类,原因是 Java 使用的是包名+类名作为类的全限定名。
使用包里的类 import
当使用另一个包里的类的时候,需要使用类的全限定名,即要带上包名。比如我们在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;
}
属性访问修饰符
- 访问修饰符是一种限制或者允许属性访问的修饰符。
- 被 public 修饰的属性是可导出的,可以被任意包中的类访问。
- 如果缺省访问修饰符,则属性只能被同一个包内的其他类和自己的对象访问到。
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";
}
}
类的全限定名
- 包名 + 类名= 类的全限定名,也可以简称为类的全名。
- 同一个 Java 程序中全限定名字不可重复。
方法
面向对象里,方法代表的是类的行为,是它们的动态描述(而属性是静态描述),比如下面的商品类进行商品描述的方法 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;
}
}
- 参数和方法里的局部变量可以认为是一样的东西。只是在方法调用之前,会用实参给参数的形参赋值。
- 方法执行完毕后,参数和方法的局部变量的数据就会被回收。
- 调用一个有返回值的方法时,就好像访问一个成员变量。
- 对象创建出来后,被放在了内存的堆上(heap) ,所以方法里创建的对象是不会随着方法结束被清除的,只要有引用指向一个对象,这个对象的数据就可以通过这个引用来访问。
方法是什么
- 方法是 Java 中代码执行的单元,是代码的载体,所有的代码,都必须属于某一个方法。
- 方法就是一串语句,加上数据输入 this 自引用和参数,执行后得到一个返回值。所以使用一个对象调用一个方法,可以叫做调用对象的方法,也可以叫做“在这个对象上调用方法( invoke a method on an object )”
- 方法不是对象的一部分,它是类的一部分。每个对象可以给成员变量赋不同的值,但是无法让方法有不同的行为。同理,无论是一个类中定义多少方法,都不会影响创建一个对象所占用的内存。
- 类通过成员变量和方法描述世界,成员变量是描述一类事物的属性,是数据;方法是描述一类事物的行为和功能,是对数据的操作。
- 方法中的代码可以通过操作一个对象的成员变量,完成一个功能。
方法隐藏的 this 自引用
方法里隐藏着一个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 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;
}
}
}
重载的方法可以调用别的重载方法,实际上,像这种补充一些缺省的参数值,然后调用重载的方法,一直调用到最后都补充好参数,调用参数最全的那个重载方法,是重载的一个重要的使用场景。
但是,不是语法要求一定要这样。重载的方法的方法体内代码可以随便写,可以不调用别的重载方法。
构造方法
- 构造方法(constructor)的方法名必须与类名一样,而且构造方法没有返回值。这样的方法才是构造方法。
- 构造方法可以有参数列表,语法与普通方法一样。
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;
}
}
- 创建对象时,构造函数的参数由 new 语句后类名后面跟的括号实参传入。
- 在一个类里,如果没有显示的添加一个构造方法,Java会给类都默认自带一个无参数的构造方法,这个方法什么也不做。
- 如果类里已经定义了构造方法,Java就不会再给类添加无参数的构造方法。这时候,就不能直接 new 一个对象时不给类名后面传参数了(除非类内自己定义了无参的构造函数)所以我们一直都在使用构造方法,这也是为什么创建对象的时候类名后面要有一个括号的原因。
- 构造方法无法被点操作符调用或者在普通方法里调用,只能通过 new 语句在创建对象的时候,间接调用。这也是为什么构造方法没有返回值的原因,因为有返回值也没有意义,new 语句永远返回的是创建出来的对象的引用。
构造方法的重载和相互调用
与普通方法一样,也能给类的构造方法定义重载方法,在构造方法里才能调用重载的构造方法。语法为: this(实参列表)。
- 构造方法不能自己调用自己,这会是一个死循环。
- 在调用重载的构造方法时,不可以使用成员变量。因为语意上讲,这个对象还没有被初始化完成,处于中间状态。
- 在构造方法里调用重载的构造方法时,调用语句必须是方法的第一行,后面可以继续有代码。
public String name;
public String id;
// >> TODO 构造方法执行前,会执行给局部变量赋初始值的操作
// >> TODO 我们说过,所有的代码都必须在方法里,那么这种给成员变赋初始值的代码在哪个方法里?怎么看不到呢?
// TODO 原来构造方法在内部变成了<init>方法。学习就是要脑洞大,敢想敢试,刨根问底。
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);
}
}
- 因为我们添加了构造方法之后,Java就不会再添加无参数的构造方法。如果需要的话,我们可以自己添加这样的构造方法,在无参的构造方法里再通过参数的缺省值去调用有参的构造方法。比如下面的
......
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;
}
}
- 静态变量使用 static 修饰符。
- 静态变量如果不赋值,Java也会给它赋以其类型的初始值。
- 静态变量一般使用全大写字母加下划线分割。这是一个习惯用法。
- 所有的代码都可以使用静态变量,只要根据访问控制符的规范,这个静态变量对其可见即可。
- 比如 public 的静态变量,所有的代码都可以使用它,但是如果是没有public修饰的静态变量,只能当前包的代码能使用它。
public static void main(String[] args){
// 使用别的类的静态变量的时候,需要使用完整形态:类名.静态变量名字
CommodityWithStatic.DISCOUNT_FOR_VIP = 0.5;
System.out.println("VIP的折扣是 " + CommodityWithStatic.DISCOUNT_FOR_VIP);
}
}
- 使用别的类的静态变量的时候,需要使用完整形态:类名.静态变量名。
- 静态变量也叫做类变量,一个静态变量在整个Java程序中只有一份,与之对比实例变量,是每个实例都有一份,所以静态变量一旦变化,所有使用这个静态变量的地方的值都会变。
静态方法
类除了静态变量外,还有静态方法。
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;
}
}
- 静态方法使用 static 修饰符方法,静态方法的定义和成员方法一样,也有方法名,返回值和参数。
- 静态方法并没有名称全部大写的约定俗成。
- 静态方法可以访问静态变量,包括自己类的静态变量和在访问控制符允许的别的类的静态变量。
- 静态方法里没有this自引用,它不属于某个实例,调用的时候也无需通过实例调用,直接用类名调用,所以它也不能通过 this 自引用的方式访问成员变量。
- 当然在静态方法里面,也可以自己创建对象,或者通过参数,获得对象的引用,进而调用方法和访问成员变量,静态方法只是没有 this 自引用而已。
静态方法通过类名调用
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);
}
}
- 静态代码块里以有任意的合法 Java 代码。
- 静态代码块可以有多个,是从上向下顺序执行的。
- 使用某个静态变量的代码块必须在静态变量后面。
- 可以认为静态代码块都被组织到了一个方法里,即 class init,会在每个class 初始化的时候被调用一次。
访问控制符
- 类,静态方法,静态变量,成员变量,构造方法,成员方法都可以使用访问控制修饰符。
- Java的访问修饰符的可见性
- public,全局可见,在包外也可以使用。
- protected,在本类、子类内可见(子类在其他包也可见,这点与缺省控制符不同)。
- 缺省,在当前包可见,对外部包不可见。
- private,只在当前类内可见。
类内部
本包
子类
外部包
private
default
default
protected
protected
protected
public
public
public
public
// 类,静态方法,静态变量,成员变量,构造方法,成员方法都可以使用访问修饰符
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;
}
}
- 成员变量应该都声明为private,如果要读写这些成员变量,最好是使用 getXXX setXXX方法,这些方法应该是public的,这样做的好处是,如果有需要,可以通过代码,检查每个属性值是否合法,这就是所谓的 getter 和 setter。
- 构造方法如果是private的,那么就只有当前的类可以调用这个构造方法,不过,有些时候会把所有的构造方法都定义成private的,然后使用静态方法调用构造方法(比如上面代码里的 createCommodity 静态方法),这样的一个静态方法还会被称为工厂方法。同样的,这样的好处是可以通过代码,检查每个属性值是否合法。
- 访问控制符的目的不只是为了限制不让人使用,而是更像是一种约定。比如 public 的方法既然外面的代码可以使用,就意味着不能乱改。比如方法签名不能随便改之类的。而对于 private 的方法,因为类外面调用不到,所以无论怎么改,也不会影响(直接影响)类外面的代码
上面的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
}
- 继承的语法就是在类名后面使用 extends 关键字加要继承的类的名字。
- 被继承的类叫做父类(Parent Class),比如本例中的Commodity类。
- 继承者叫做子类(Sub Class),比如本例中的Phone类。
- Java中的继承是单继承,即一个类只允许有一个直接的父类.
- 没错,别的类也可以继承子类,比如可以有一个HuaweiPhone继承 Phone 这时候,HuaweiPhone就是Phone的子类了。
- 子类继承了父类 (这里说的父类也包括祖先类) 的属性和方法。但是子类并不能访问父类的private的成员(包括方法和属性)。
- 使用子类的实例可以访问父类的公共属性和方法,就好像可以一物二用一样,子类的实例既可以当做子类的引用使用,也可以当成父类的引用使用。
看下面的例程
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调用父类的方法
使用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;
}
......
}
- 注意:使用super调用父类的构造方法,必须是子类构造方法的第一个语句。
父类和子类引用赋值的关系
可以用子类的引用给父类的引用赋值,也就是说,父类的引用可以指向子类的对象,但是反之则不行,不能让子类的引用指向父类的对象。因为父类并没有子类的属性和方法。
因为子类继承了父类的方法和属性,所以父类的对象能做到的,子类的对象肯定能做到。换句话说,我们可以在子类的对象上,执行父类的方法。
instanceof 操作符
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");
}
}
}
final 修饰符
- final 修饰类,那么该类就不能被继承,比如 Java 的字符串 String 类不允许被扩展,就是通过final实现的
implements java.io.Serializable, Comparable<String>, CharSequence {
}
- final 修饰成员变量,那么成员变量在构造方法或者声明时初始化后不允许被修改(必须赋值,只能赋值一次,且不允许在其他地方赋值)
- final 修饰静态变量与成员变量的情况类似,静态变量必须被赋值(声明或者在static块里赋值)且不能被更改。
- final 修饰方法,该方法不允许被子类覆盖。
- final 不能修饰构造方法。
- final 修饰形参/局部变量,在函数内不能再给形参/局部变量 赋值。