2-1-类和对象
上一级:系统整理java知识体系
类的定义及使用
类的定义
类是构造对象的模板和蓝图
-
由类构造对象的过程称为"创建类的实例(instance)"
-
封装(亦称为数据隐藏),就是将数据和行为组合到一个包(package)中,并对对象的使用者隐藏具体的实现方式。
封装是处理对象的一个重要概念。
-
对象中的数据称为实例字段(instance field)
-
操作数据的方法称为方法(method),C++中使用函数实现
-
状态(state)是当前对象的所有当前状态的集合
-
通过扩展一个类来建立另外一个新类的过程称为继承(inheritance),值得注意的是继承一般会是一个更特殊的类继承一个更一般的类,同时会增加一些新的方法来处理所需的需求.
对象
对象是包含对用户公开的特定功能部分和隐藏的实现部分的集合
-
每个对象都有三个主要特性
-
对象的行为(behavior)
-
对象的状态(state)
-
对象的标识(identity)
-
对应到Java中,分别是
行为 -> 可调用的方法 methods
状态 -> 描述当前状况的信息,比如各种变量
标识 -> 对象(或称对象实例)的名称
要注意的的是对象的状态会随着时间而发生改变,这种改变不能是自发的,否则就破坏了封装性
类的使用
Java中,使用构造器(constructor,或称构造函数)来构造对象实例。
-
构造器可以构造并初始化对象
-
构造器名字与类名相同,使用时在构造器前加上new操作符,如下:
1 | new Date(); |
- 构造器构造出对象后,可以用一个对象变量来引用他,便于以后继续使用
1 | Date birthday = new Date(); |
- 对象变量(如birthday)不是对象,实际上若没有对其指定初始状态(即赋初值),其就没有引用任何对象,这时就不能对这个对象变量使用对象(Date)可使用的方法.
若要使用则必须先初始化,有两种初始化方法
1 | deadline = new Date(); |
- 可以显式的将对象变量设置为null表示其目前没有引用任何对象
1 | deadline = null; |
- 所有的Java对象都储存在堆上,当一个对象包含另外一个对象变量时,它只是包含了指向另外一个堆对象的指针。
静态工厂方法(factory method)
- 封装的意义就在于程序员不用考虑其内部的实例字段和具体方法实现,只需要考虑类对外提供的方法。
- 更改器方法和访问器方法
- 更改器方法会修改对象的状态
- 访问器方法只访问对象而不修改对象
LocalDate类和Date类
1 | //java.time.LocalDate; |
1 | import java.time.*; |
多个源文件
- 可以任务Java的编译器内置了Unix或者Windows的make功能
- 多个源文件编译有两种命令格式.
其中第二钟方式没有显式地编译Employee.java,但当Java编译器发现EmployeeTest.java使用了Employee类时,他会自动查找Employee.class文件,
如果没找到,就会寻找Employee.java并对它进行编译
javac Employee*.java
//或者
javac EmployeeTest.java
构造器
- Java中的构造器总是要和new操作符一起使用,因为所有的Java对象都是在堆上构造的。初学者最容易犯的错误是忘记new操作符
var,声明局部变量
- Java10中薪增了一个var关键字,用于声明局部变量,如果可以从变量的初始值直接推导出他们的类型,则无须在等号左侧指定类型,这种情况下可以使用var关键字
- 要注意的是对应基本数值类型,如int,long,double。最好不要滥用var关键字,建议少用或者不用,以此帮你你培养对数值精度的敏感性。
1 | //例如 |
使用null 引用
类的访问权限
私有方法
final实例字段
静态属性和方法
静态字段
如果将一个字段定义为static,这个字段就成为这个类所有对象共享访问的唯一标识码.
静态字段也被成为类字段,“静态”是沿用了C++的叫法,无实际意义。
静态常量
静态常量经常使用,如Math类中定义了一个静态常量PI
1 | public class Math{ |
在程序中访问静态常量可以通过Math.PI访问
类似的System.out中也有静态常量即 System.out.PrintStream
原生方法
即不是在Java语言中实现的方法,可以绕过Java的访问控制机制,实现修改final变量的值
静态方法
静态方法即不在对象上执行的方法,一般通过类名.方法的方式调用
1 | Math.pow(x,a) |
- 也可以使用对象来调用静态方法(例如用harr.getNextId()代替Employee.getNextId()),这是合法的。但这种写法容易造成阅读上的语义混淆,不建议这样写
下面两种情况下可以使用静态方法:
- 方法不需要访问对象状态,因为这个方法所需要的所有参赛都显式的通过传入的参数提供,如Math.pow()
- 方法只需要访问类的静态字段,如Employee.getNextId()
工厂方法
静态方法的另外一种常见用途就是静态工厂方法(factory method)
1 | LocalDate.now() |
下面给出常见的工厂方法的调用
1 | NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(); |
按值调用
Java总是按值调用的,即方法得到的所有参数值的一个副本,方法不能修改传递给他的任何参赛变量的内容,只是新建了一个副本,在这个副本基础上修改。方法结束时会调用gc,对副本进行销毁
- 但把对象引用作为参数时就不同了,由于传递到方法中的对象引用和原来的对象引用实际上是在指向同一个堆上的对象,所以就可以达到修改这个堆上对象的作用
方法参数总结
- 方法不能修改基本数据类型的参数(即数值型,布尔型等)
- 方法可以改变对象参数的状态。(通过对象引用)
- 方法不能让一个对象参数引用一个新的对象,(他们在方法内实现这些操作,但最后改变的只是对象引用的副本,并没有把改变的结果传递到方法外)
例如形参x、y交换了,实参a、b没有受到影响
src/v1ch04/ParamTest/ParamTest.java
1 | /** |
对象构造
重载
显式字段初始化
Java中提供了下面的写法对类中的实例字段进行初始化
1 | class Empoyee{ |
-
C中不能直接初始化类的实例字段,所有的字段都必须放到构造器中设置,但C也提供了一种特殊语法,称作"初始化器列表语法"来达成对类的实例字段进行初始化的目的。
但Java中不需要这么麻烦,因为Java中的对象没有字对象,只有指向其他对象的指针,C++这么做是因为其类中可以有子对象,所以用这样的初始化方法来防止错误的语法,即对子对象进行初始化
finalize
- 不要使用finalize方法来完成清理,这个方法已经被废弃。
- 可以改用close方法或者Runtime.addShutdownHook等方法。
- Java9以后可以使用Cleaner类来注册一个动作。
继承
-
Java中,所有的继承都是公共继承(public),没有C++中的保护和私有继承
-
超类和子类,分别对应其他程序设计语言中的双亲类和孩子类
超super和子sub来自于计算机科学与数学理论中的集合语言的术语。
所有员工组成的集合包含所有经理组成的集合。
即员工集合是经理集合的超集,反过来说,经理集合是员工集合的子集
- 设计类的时候,应当将最一般的方法放在超类中,而将更特殊的方法放到子类
子类构造器
- 子类构造器中调用超类的构造器,这条语句一定要写在子类构造器中的第一行
1 | pubulic Manager(String name,double salary,int year,int month,int day){ |
类似与C++中在初始化列表中对超类进行构造
1 | //C++ |
多态、动态绑定、虚拟(virtual)、final
对于下面代码
1 | // construct a Manager object |
多态的含义
这里的e虽然声明为Employee类型,但当e引用staff[1]/staff[2]时,它可以引用Employee类型的对象,当e引用staff[0]时,也可以引用Manger类型的对象。
- 一个对象变量(如e)可以指示多种实际类型的现象称为多态
- 在运行时可以自动地选择适当的方法,称为动态绑定
- C++中,要实现动态绑定,需要将成员函数(方法)声明为virtual
- 而在Java中默认是动态绑定的。如果不想让一个方法动态绑定(即虚拟的),则要单独把他标记为final
1 | //这种调用是可以的 |
这个例子中,虽然staff[0]和boss引用同一个对象,但上面那种调用可以,下面那种调用却不行。
这是因为staff[0]声明的类型是Employee,而setBonus不是Employee类的方法。
boss声明的类型是Manger。
注意对象引用和对象的区别。staff[0]已经有了指向,我们说是对象,而e是对象引用,e既可以指向Employee,也可以指向Manager.
1 | /* |
1 | /* |
不能将超类的引用赋给子类,例如,下面的赋值是非法的
1 | Manager m = staff[i]; |
因为不是所有的员工都是经理,如果赋值成功,m就有可能引用了一个不是经理的Employee对象,这样m.setBonus就会报错。
多重继承
多重继承是一个子类可以有多个超类
Java不支持多重继承,但是提供了类似多重继承的功能,详情查看接口
- C++中,一个类可以有多个超类
可见性
子类方法不能低于超类方法的可见性
即当超类是public时,子类不能是public,可以是public
经常出现的错误是忘记了子类的public修饰符,导致编译器报错
提示你试图提供更加严格的访问权限(private)
final类
不允许扩展的类被称为final类
- 如你希望阻止人们派生出Executive的子类,就可以在声明Executive类时使用final修饰符。
1 | public final class Executive extends Manager{ |
- 将类中某个特定方法声明为final,这样子类就不能覆盖这个方法(final类中的所有方法自动称为final方法)
1 | ... |
强制类型转换
子类转换到超类是允许的,前面已经提到了。
但超类转换成子类却要受到需要限制
在Java中,试图进行从超类到子类的转换时需要使用强制类型转换,形如
1 | Manager boss = (Manager) staff[0]; |
在运行时,Java的运行时系统会注意到这里,检查你的承诺是否符合,如果不符,将会抛出一个ClassCastException异常。
糟糕的是,如果没有捕获到这个异常,程序会自动的终止。
为此,我们应当使用instanceof操作符来检查,是否可以实现从超类到子类的强制类型转换(即检查你的承诺是否符合)
形如
1 | if(staff[1] instanceof Manager){ |
这样,编译器会检查你的这个转换是否能成功,如果不可能成功就不会让你完成这个转换.例如下面的这个强制类型转换
1 | String c = (String)staff[1]; |
明显的String不是Employee的子类,这个转换不可能成功,所以会编译错误
综上:
- 只能在继承层次内进行强制类型转换,如
1 | Manager boss = (Manager) staff[0]; |
- 在超类转换成子类之前要使用instanceof运算符来进行检查
此外如果x为null,
x instanceof C ,
并不会产生异常,而是返回false。
这是因为null没有引用任何对象,自然也不会引用C类型的对象
- 强制类型转换并不是一种好的做法,一般只用在当必须在超类对象上调用子类特有的方法时。
- Java中必须要将instanceof运算符与强制类型转换结合起来使用
受保护访问protected
保护字段
- Java中,保护字段只能由同一个包中的类访问。这是一个限制机制,防止通过派生子类来访问受保护的字段.
假设包B只有子类Administrator,它是Employee的子类。同时Employee的其他子类都在包A中,这样Administrator类中的方法不能查看包A中其他的Employee对象的hireDay字段
保护方法
保护方法是更有实际意义的做法
如果需要限制某个方法的使用,即某个超类只信任他的子类,只希望他子类来正确地使用这个方法,而(不是他的子类的)其他的类不能正确使用这个方法,就可以把这个方法声明为protected
有一个很好的示例是Object类中的clone方法,这个方法是protected的
事实上,Java中的受保护及对所有的子类(无论在不在同一个包),及同一个包中所有其他的类(无论是不是子类)都可见
简化一下说法就是,Java中的protected对同一个包中所有类+另外一个包中的子类都可见,他们都可以访问。
Java的4个访问控制修饰符
1.仅对本类可见–private
2.对外部完全可见–public
3.对本包和所有子类可见–protected
4.对本包可见–默认,不需要修饰符
Java中的继承extends只能是public继承,不存在C++中的protected继承和private继承
同一个包下所有的子类和非子类
在 《Thinking in Java》 中,protected 的名称是「继承访问权限」,这也就是我们记忆中的 protected:protected 必须要有继承关系才能够访问。 所以你以为你懂了, 可是你真的理解了这句话吗?
先思考几个问题:
同一个包中, 子类对象能访问父类的 protected 方法吗?
不同包下, 在子类中创建该子类对象能访问父类的 protected 方法吗?
不同包下, 在子类中创建父类对象能访问父类的 protected 方法吗?
不同包下, 在子类中创建另一个子类的对象能访问公共父类的 protected 方法吗?
父类 protected 方法加上 static 修饰符又会如何呢?
《Thinking in Java》中有一句话:「protected 也提供包访问权限, 也就是说,相同包内的其他类可以访问 protected元素」, 其实就是 protected 修饰符包含了 default 默认修饰符的权限, 所以第 1 个问题你已经知道答案了, 在同一个包中, 普通类或者子类都可以访问基类的 protected 方法。
作者:云大数据社区
链接:https://juejin.im/post/6844903517988061191
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
父类为非静态(no static) protected 修饰类
1 | package com.protectedaccess.parentpackage; |
不同包下子类访问父类的protected方法
若子类与基类不在同一包中,那么在子类中,子类实例访问的实际上是其从基类继承而来的protected方法,
子类中实际上把父类的方法继承下来了, 可以通过该子类对象访问, 也可以在子类方法中直接访问, 还可以通过 super 关键字调用父类中的该方法。
1 | package com.protectedaccess.parentpackage.sonpackage1; |
不同包下,在子类中通过[父类引用]不可以访问父类 protected 方法
不同包下,在子类中不可以,通过[父类引用]访问其 protected 方法
无论是创建 Parent 对象还是通过多态创建 Son1 对象, 只要是父类引用( Parent 引用), 则不可访问, 编译器会提示错误。
1 | package com.protectedaccess.parentpackage.sonpackage1; |
不同包下,在子类中不能通过[另一个子类引用]访问共同基类的 protected 方法
1 | package com.protectedaccess.parentpackage.sonpackage2; |
//注意 Son2 是另一个子类, 在 Son1 中创建 Son2 的对象是无法访问他们共同的父类的 protected 方法的
1 | package com.protectedaccess.parentpackage.sonpackage1; |
父类为静态 protected 修饰类
对于protected的静态变量, 在子类中可以直接访问, 在不同包的非子类中则不可访问
1 | package com.protectedaccess.parentpackage; |
静态方法直接通过类名访问
无论是否同一个包,在子类中均可直接访问
1 | package com.protectedaccess.parentpackage.sonpackage1; |
在不同包下,非子类不可访问
1 | package com.protectedaccess.parentpackage.sonpackage1; |
看到这里你应该知道有多少种情况了, 针对不同的情况都可能出现意外的结果, 所以还是得多实践, 仅仅在书上看一遍 protected 修饰符的作用是无法真正发现它的微妙。
继承的设计技巧
- 将公共操作和字段放在超类中
例如把name放入Person类,而不是重复放在Employee和Student类中
- 不要使用受保护的字段.
protected机制并不能带了更多的保护,这是因为- 子类集合是无限制的,任何人都能在你的类的基础上派生出一个子类,然后通过这个子类访问protected字段
- Java中,同一个包中所有的类都能访问protected实例字段,而不管它们是否为这个类的子类
- 使用继承实现“is-a”关系
钟点工与员工之间不属于"is-a"关系,既钟点工不是特殊的员工。使用员工派生出钟点工不是一个好主意。这是因为钟点工会继承员工类的工资字段,而它自己又定义了一个时薪字段,这会带来无尽的麻烦,当你想要打印一份薪水单或者工资单时。
- 除非所有继承来的方法都是有意义的,否则不要使用基础
- 在覆盖方法时,不要改变预期的行为
例如某个方法预期要对day字段+1,那么在他的子类中你最好不要改变中国方法,把+1变成了-1,这会给你自己带来后续的迷惑。
- 使用多态,而不用使用类型信息
例如
if(x is of type 1)
action(x);
else if(x is of tyoe 2)
action(x);
这样的情况更改考虑使用多态,把action方法放到两个type的超类或者接口中,然后调用x.action()解决问题。
即使用多态固有的动态分派机制执行正确的操作。
使用多态方法或接口实现的代码比使用多个类型检查的代码更加易于维护或者扩展。
- 不要滥用反射
如果使用反射,编译器将无法帮助你找到编程错误,因为这些错误都发生在运行时,编译器帮不了你太多。只有在运行时才会发现错误并导致异常。
Object类
实际使用Object时,需要使用强制类型转换,转换为具体的对应的类型的对象,才能使用相对应类型的方法
包管理
包名
使用包名的主要原因是为了确保类名的唯一性,如果有两个相同的Employee类,将其放在不同的包中,就不会产生冲突。
为了保证包名的唯一性,采用将一个域名逆序的形式书写。如果com.horstmann.corejava
在编译器看来,嵌套的包之间没有任何关系,例如java.util包与java.util.jar包毫无关系。每个包都是一个独立的类集合。
类的导入
一个类可以使用所属包的所有类,以及其他包中的公共类。
有两种方式来访问另外一个包中的公共类。
第一中方式是使用完全限定名,就是包名后面跟着类名
1 | java.time.LocalDate today = java.time.LocalDate.now(); |
另外一种方式是使用import语句,使用import语句后,在使用类名时,就不必写出类的全名了
1 | import java.time.*; |
即无需在前面加上包的前缀
还可以导入一个包中特定的一个类
1 | import java.time.LocalDate; |
1 | 注意,只能使用*导入一个包,不能使用形如import java.* 或者 import java.* 等语句一次导入多个包。 |
如果要编译器无法确定你想使用的是哪个Date类(有多个Date类),可以另起一行,新写一个特定的import语句
1 | import java.util.*; |
如果两个Date类都要使用,那么就要在每个Date类前加上完整的包名
1 | var deadline = new java.util.Date(); |
- 类中的字节码总是使用完整的包名来引用其他包的类
- 可以任务Java中的package和import语句相当于C++中的namespace 和using指令。
静态导入
使用下列这种import语句允许导入静态方法和静态字段而不只是类
1 | import static java.lang.System.*; |
这样就可以使用System中的静态方法和静态字段而不用加类名前缀
1 | out.println("HelloWorld!");//i.e.,System.out |
也可以导入特定的静态方法和静态字段
1 | import static java.lang.System.out; |
枚举
定义枚举类型
1 | public enum Size{SMALL,MEDIUM,LARGE,EXTRA_LARGE} |
实际上这个声明定义的类型是一个类,他刚好有四个实例,且不可能构造新的对象了
比较乐观枚举类型的值时,不需要调用equals,直接使用"=="就可以了
此外,在需要的时候,也可以给枚举类型增加构造器,方法和字段
构造器只是在构造枚举常量的时候调用
1 | enum Size |
枚举的构造器总是私有的,所有可以省略private修饰符。
所有的枚举类型都是Enum类的,继承了这个类中的许多方法
toString方法
例如Size.SMALL.toString()将返回字符串"SMALL"
valueOf方法
与toString相对的逆方法是valueOf,例如
1 | Size s = Enum.valueOf(Size.class,"SMALL"); |
将s设置成Size.SMALL
此外,枚举类型都有一个静态的values方法,它将返回一个包含全部枚举值的数组
1 | Size[] values = Size.values(); |
将返回包含元素Size.SMALL,Size.MEDIUM,Size.LARGE和Size.EXTRA_LARGE的数组
ordinal方法
返回enum声明中枚举常量的位置。位置从0开始计数,例如Size.MEDIUM。ordinal()返回1.
下面是EnumTest的程序
1 | package enums; |
反射
能够分析类能力的程序称为反射,反射的功能
-
在运行时分析类的能力
-
在运行时检查对象,例如,编写一个适用于所有类的toString方法
-
实现泛型数组操作代码
-
利用Method对象,这个对象很像C++中的函数指针。
-
下一节: java接口与抽象类