方法
一个class可以包含多个field,直接把field用pulic暴露给外部可能会破坏封装性。为了避免外部代码直接去访问field,我们可以用private修饰field,拒绝外部访问。
我们需要使用方法来让外部代码可以间接修改field
调用方法setName()和setAge()来间接修改private字段,在方法内部,我们就有机会检查参数对不对。同样,外部代码不能直接读取private字段,但是可以通过getName()和getAge()间接获取private字段的值。
所以,一个类通过定义方法就可以给外部代码暴露一些操作的接口,同时内部自己保证逻辑一致性。
定义方法
定义方法的语法是
方法返回值通过return 语句实现,如果没有返回值,返回类型设置为void,可以省略return。
private方法
和private字段一样,private方法不允许外部调用。定义private方法是内部方法可以调用private方法。
this变量
在方法内部,可以使用一个隐含的变量this它始终指向当前实例。因此,通过 this.field就可以访问当前实例的字段。
如果没有命名冲突,可以省略this。但是,如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this。
方法参数
方法可以包含0个或任意个参数。方法参数用于接收传递给方法变量值。调用方法时,必须严格按照参数的定义---传递。
可变参数
可变参数用类型...定义,相当于 数组类型
调用时
可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null
参数绑定
调用方把参数传递给实例方法时,调用时传递的值会按参数位置---绑定。
基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
引用类型参数的传递,调用方的变量和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方。
构造方法
创建实例的时候,实际上是通过构造方法来初始化实例的。
构造方法的名称就是类名,参数没有限制,在方法内部,也可以编写任意语句。构造方法没有返回值,调用构造方法,必须用new操作符。
默认构造方法
如果一个类没有定义构造方法,编译器会自动为我们生成一个构造方法,它没有参数,也没有执行语句。如果我们自定义了一个构造方法,那么编译器就不再自动创建默认构造方法。如果既要能使用带参数的构造方法,有想保留不带参数的构造方法,那么只能把两个构造方法都定义出来。
没有在构造方法中初始化字段时,引用类型的字段默认是null,数值类型的字段用默认值,int类型是默认值是0,布尔类型默认值是false。
既对字段进行初始化,又在构造方法中对字段进行初始化的情况:在Java中,创建对象实例的时候,按照先初始化字段、再执行构造方法的代码进行初始化,因此构造方法的代码由于后运行,字段值最终由构造方法的代码确定。
多构造方法
可以定义多个构造方法,在通过new操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分。
一个构造方法可以调用其他构造方法 ,这样做的目的是便于代码复用。调用其他构造方法的语法是this(...)
方法重载
在一个类中,我们可以定义多个方法。如果有一系列方法,他们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。这种方法名相同,但各自的参数不同,称为方法重载。ps:方法重载的返回值类型通常都是相同的。
方法重载的目的是,功能类似的方法使用同一名字,更容易记住,调用起来更简单。
继承
继承是面向对象编程中非常强大的一种机制,可以复用代码。用extends关键字来实现继承。子类自动获得父类的所有字段,严禁定义与父类重名的字段。(超类、父类、基类)(子类、扩展类)
继承树
在Java中,没有明确写extends的类,编译器会自动加上extends Object。所以,除了Object其他任何类都会继承自某个类。
Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。Object没有父类。
protected
继承有个特点,就是子类无法访问父类的private字段或者private方法。这使得继承的作用被削弱了,为了让子类可以访问父类的字段,我们需要把private改为protected。用protected修饰的字段可以被子类访问。protected关键字可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以
及子类的子类所访问。
super
super关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName。
如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
阻止继承
正常情况下,只要某个class没有final修饰符,那么任何类都可以从该class继承。
从Java15开始,允许使用sealed修饰class,并通过permits明确写出能够从该class继承的子类名称。
向上转型
把一个子类类型安全的变为父类类型的赋值被称为向上转型。实际上是把一个子类型安全地变为更加抽象的父类型。
向下转型
把一个父类类型强制转型为子类类型,就是向下转型。向下转型很可能会失败,因为子类的功能比父类多,多的功能无法凭空变出来。为了避免向下转型出错,Java提供了instanceof操作符,可以先判断一个实例究竟是不是某种类型。
instanceof实际上判断一个变量所指向的实例是否是指定类型,或者是这个类型的子类。如果一个引用变量为null,那么对任何instanceof的判断都为false。
从Java14开始,判断instanceof后,可以直接转型为指定变量,避免再次强制转型。
区分继承和组合
在使用继承时,要注意逻辑一致性。继承是is关系,组合是has关系。
多态
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写。
Override和Overload不同的是,如果方法签名不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override。(方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在Java程序中,出现这种情况,编译器会报错。)
加上@Override
可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。但是@Override
不是必需的。
Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。这个非常重要的特性在面向对象编程中这称之为多态。
多态是指,针对某个类型的方法调用,真正执行的方法取决于运行时期实际类型的方法。多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。
覆写Object 方法
所有的class最终都继承自object,而object 定义了几个重要方法:
toString():把instance输出为String;
equals():判断两个instance是否逻辑相等;
hashCode():计算一个instance的哈希值。
在必要情况下,我们可以覆写Object的这几个方法。
调用super
在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。
final
继承可以允许子类覆写父类的方法。
如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final。用final修饰的方法不能被override。
如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final。
对一个类的实例字段,同样可以用final修饰,用final修饰的字段在初始化后不能被修改。
抽象类
如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类覆写它,那么,可以把父类的方法声明为抽象方法:
把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,person类也无法被实例化,必须把person类本身也声明为abstract,才能争取编译它。
抽象类
如果一个class定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract修饰。因为无法执行抽象方法,因此这个类也必须申明为抽象类。
因为抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。
面向抽象编程
面向抽象编程的本质是:
上层代码只定义规范;
不需要子类就可以实现业务逻辑(正常编译);
具体的业务逻辑由不同的子类实现,调用者并不关心。
接口
在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现。如果一个抽象类没有字段,所有方法全部都是抽象方法:
就可以把该抽象类改写为接口:interface。
所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。
当一个具体的class去实现一个interface时,需要使用implements关键字。
在Java中,一个类只能继承自另一个类,不能从多个类继承,但是,一个类可以实现多个interface。
术语
Java的接口特指interface的定义,表示一个接口类型和一组方法签名,而编程接口泛指接口规范,如方法签名,数据格式,网络协议等。
接口继承
一个interface可以继承自另一个interface。interface继承自interface使用extends,它相当于扩展了接口的方法。
继承关系
合理设计interface和abstract class的继承关系,可以充分复用代码。一般来说,公共逻辑适合放在abstract class中,具体逻辑放到各个子类中,而接口层次代表抽象程度。
在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口 比抽象类更抽象。
default方法
在接口中,可以定义default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
静态字段和静态方法
在一个class中定义的字段,我们称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响。还有一种字段是用static修饰的字段,称为静态字段:static field。实例字段在每个实例中都有自己的一个独立空间,但是静态字段只有一个共享空间,所有实例都会共享该字段。
对于静态字段,无论修改哪个实例的静态字段,效果都是一样的:所有实例的静态字段都被修改了,原因是静态字段并不属于实例。
虽然实例可以访问静态字段,但是它们指向的其实都是person class的静态字段。因此,不推荐用实例变量.静态字段去访问静态字段,因为在Java程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段来访问静态对象。
静态方法
用static修饰的方法称为静态方法。调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。因为静态方法属于class而不属于实例,因此,静态方法内部无法访问this变量,也无法访问实例字段,只能访问静态字段。
接口的静态字段
因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的,并且静态字段必须为final类型。
包
在Java中,我们使用package来解决名字冲突。
Java定义了一种名字空间,称之为包。一个类总是属于某个包,类名只是一个简写,真正的完整类名是包名.类名。
在定义class的时候,我们需要在第一行声明这个class属于哪个包。在Java虚拟机执行的时候,JVM只看完整类名,因此,只要包名不同,类就不同。
包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。
没有定义包名的class,它使用的是默认包,非常容易引起名字冲突,因此,不推荐不写包名的做法。
包作用域
位于同一个包的类,可以访问包作用域的字段和方法。不用public、protected、private修饰的字段和方法就是包作用域。
import
在一个class中,我们总会引用其他的class。引用有三种写法:
第一种,直接写出完整类名:
第二种,用import语句:
在写import的时候,可以使用*,表示把这个包下面的所有class都导入进来(但不包括子包的class)。我们一般不推荐这种写法,因为在导入了多个包后,很难看出arrays类属于哪个包。
第三种,import static的语法,可以导入一个类的静态字段和静态方法。
Java编译器最终编译出的.class文件只使用完整类名,因此,在代码中,当编译器遇到一个class名称时:
如果是完整类名,就直接根据完整类名查找这个class;
如果是简单类名,就按下面的顺序依次查找:
查找当前package是否存在这个class;
查找import的包是否包含这个class;
查找java.lang包是否包含这个class。
如果按照上面的规则还无法确定类名,则编译报错。
编写class的时候,编译器会自动帮我们做两个import动作:
默认自动import当前package的其他class;
默认自动import java.lang.*。
自动导入的是java.lang包,但是类似java.lang.reflect这些包仍需要手动导入。
如果有两个class名称相同,例如,mr.jun.Arrays和java.util.Arrays,那么只能import其中一个,另一个必须写完整类名。
作用域
public、protected、private这些修饰符可以用来限定访问作用域。
public
定义为public的class、interface可以被其他任何类访问
private
定义为private的field、method无法被其他类访问。private访问权限被限定在class的内部,而且与方法声明顺序无关。如果一个类内部还定义了嵌套类,那么嵌套类拥有访问private的权限。定义在一个class内部的class称为嵌套类。
protected
protected用作于继承关系,定义为protected的字段和方法可以被子类以及子类的子类访问。
package
包作用域是指一个类允许访问同一个package的没有public、private修饰的class、字段和方法。
局部变量
在方法内部定义的变量称为局部变量,局部变量作用域从变量声明处开始到对应的块结束。方法参数也是局部变量。
final
用final修饰class可以阻止被继承;用final修饰方法可以阻止被子类覆写;用final修饰field可以阻止被重新赋值;用final修饰局部变量可以阻止被重新赋值。
最佳实践
如果不确定是否需要public,就不声明为public,即尽可能少地暴露对外的字段和方法。
把方法定义为package权限有助于测试,因为测试类和被测试类只要位于同一个package,测试代码就可以访问被测试类的package权限方法。
内部类
在Java程序中,通常情况下,我们把不同的类组织在不同的包下面,对于一个包下面的类来说,它们是在同一层次,没有父子关系:
还有一种类,它被定义在一个类的内部,称为内部类。
Inner Class
如果一个类定义在另一个类的内部,这个类就是Inner Class
它与普通类有个最大的不同,就是Inner Class的实例不能单独存在,必须依附于一个OuterClass的实例。
要实例化一个Inner,必须首先创建一个Outer的实例,然后,调用Outer实例的new来创建Inner实例。
Inner Class和普通class相比,除了能引用Outer实例外,还有一个额外的“特权”,就是可以修改 Outer Class的private字段。
Anonymous Class
还有一种定义Inner Class的方法,它不需要在Outer Class中明确地定义这个Class,而是在方法内部,通过匿名类来定义。
classpath和jar
classpath是JVM用到的一个环境变量,它用来指示JVM如何搜索class。
因为Java是编译型语言,源码文件是.java,而编译之后的.class文件才是真正可以被JVM执行的字节码。因此,JVM需要知道,如果加载一个abc.xyz.Hello的类,应该去哪搜索对应的Hello.class文件。
所以,classpath就是一组目录的集合,它设置的搜素路径与操作系统相关。
现在我们假设classpath
是.;C:\work\project1\bin;C:\shared
,当JVM在加载abc.xyz.Hello
这个类时,会依次查找:
classpath的设定方法有两种:
在系统环境变量中设置classpath环境变量,不推荐,会污染整个系统环境;
在启动JVM时设置 classpath变量,推荐。
不要把任何Java核心库添加到classpath中,JVM根本不依赖classpath加载核心库。
jar包
jar 包可以把package组织的目录层级,以及各个目录下的所有文件都打成一个jar 文件。jar包实际上就是一个zip格式的压缩文件,而jar包相当于目录。如果我们要执行一个jar包的class,就可以把jar包放到classpath中。
class版本
只要看到UnsupportedClassVersionError就表示当前要加载的class文件版本超过了JVM的能力,必须使用更高版本的JVM才能运行。
指定编译输出有两种方式,一种是在javac命令行中用参数--release设置;第二种方式是参数--source指定源码版本,用参数--target指定输出class版本。
源码版本
在编写源代码的时候,通常会预设一个源码版本。在编译的时候,如果用--source或--release指定源码版本,则使用指定的源码版本检查语法。
高版本的JDK可编译输出低版本兼容的class文件,低版本的JDK可能不存在高版本JDK添加的类和方法。
模块
JVM自带的标准库rt.jar不要写到classpath中,写了反而会干扰JVM的正常运行。如果漏写了某个运行时需要用到的jar,那么在运行期极有可能抛出ClassNotFoundException。所以,jar只是用于存放class的容器,它并不关心class之间的依赖。
自带“依赖关系”的class容器就是模块。
把一堆class封装为jar仅仅是一个打包的过程,而把一堆class封装为模块则不但需要打包,还需要写入依赖关系,并且还可以包含二进制代码(通常是JNI扩展)。此外,模块支持多版本,即在同一个模块中可以为不同的JVM提高不同的版本。
编写模块
创建模块和原有的创建Java项目是完全一样的。
其中,bin
目录存放编译后的class文件,src
目录存放源码,按包名的目录结构存放,仅仅在src
目录下多了一个module-info.java
这个文件,这就是模块的描述文件。
其中,module是关键字,后面的hello.world是模块的名称,它的命名规范与包一致。花括号的requires xxx表示这个模块需要引用的其他模块名。当我们使用模块声明了依赖关系后,才能使用引入的模块。模块的重要作用就是声明依赖关系 。
运行模块
要运行一个模块,我们只需要指定模块名。
访问权限
Java的class访问权限分为public、protected、private和默认的包访问权限。引入模块后,这些访问权限的规则就要稍微做些调整。
确切地说,class的这些访问权限只在一个模块内有效,模块和模块之间,例如,a模块要访问b模块的某个class,必要条件是b模块明确地导出了可以访问的包。