Java完全支持面向对象的三种基本特征:继承、封装和多态。
其中封装指的是将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能;
继承是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法;
多态值得是在执行同一个方法时,可能表现出多种行为特征。
面向对象的方式实际上由OOA(面向对象分析)OOD(面向对象设计)OOP(面向对象编程)三个部分有机组成。
Java语言是一门强类型语言。强类型包含两方面的含义:1.所有的变量必须先声明、后使用;2.指定类型的变量只能接受类型与之匹配的值。
Java语言支持的类型分为两种:基本类型(Primitive Type)和引用类型(Reference Type)。
基本类型包括boolean类型和数值类型。数值类型有整数类型和浮点类型。整数类型包括byte、short、int、long、char,浮点类型包括float、double。
char代表字符型。实际上字符型也是一种整数类型,相当于无符号整数类型。
字符型值有如下三种表示形式:
直接通过单个字符来指定字符型值,例如'A'、'9'、'0'。
通过转义字符表示特殊字符型值,例如'\n'、'\t\'等。
直接使用Unicode值来表示字符型值,格式是'\uXXXX',其中XXXX代表一个十六进制的整数。
浮点数
Java提供了三个特殊的浮点数值:正无穷大、负无穷大和非数,用于表示溢出或出错。例如,使用一个正数除以0将得到正无穷大,使用一个负数除以0将得到负无穷大,0.0除以0.0或对一个负数开方将得到一个非数。正无穷大通过Double或Float类的POSTIVE_INFINITY表示;负无穷大通过NEGATIVE_INFINITY表示,非数通过Double或Float类的NaN表示。
必须指出的是,所有的正无穷大数值都是相等的,所有的负无穷大数值都是相等的;而NaN不与任何数值相等,甚至和NaN都不相等。
只有浮点数除以0才可以得到正无穷大或负无穷大,因为Java语言会自动把和浮点数运算的0(整数)当成0.0(浮点数)处理。如果一个整数值除以0,则会抛出一个异常:ArithmeticException:/by zero。
从JDK1.7开始引入了一个新功能:程序员可以在数值中使用下划线,不管是整数数值,还是浮点数数值,都可以自由地使用下划线。通过使用下划线分隔,可以更直观地辨认数值中到底包含多少位。
位运算符
Java支持的位运算符有如下7个:
&:按位与。当两位同时为1时才返回1。
|:按位或。只要有一位位1即可返回1。
~:按位非。单目运算符。将操作数的每个位(包括符号位)全部取反。
^:按位异或。当两位相同时返回0,不同时返回1。
<<:左移运算符。
>>:右移运算符。
>>>:无符号右移运算符。
左移运算符是将操作数的二进制码整体左移指定位数,左移后右边空出来的位以0补充。
Java右移运算符有两个:>>和>>>,对于>>运算符而言,把第一个操作数的二进制码右移指定位数后,左边空出来的位以原来的符号位填充,即如果第一个操作数原来是正数,则左边补0;如果第一个操作数是负数,则左边补1。>>>是无符号右移运算符,它把第一个操作数的二进制码右移指定位数后,左边空出来的位总是以0填充。
进行移位运算时还要遵循如下规则:
对于低于int类型(如byte、short和char)的操作数总是先自动类型转换为int类型后再移位。
对于int类型的整数移位a>>b,当b>32时,系统先用b对32求余(因为int类型只有32位),得到的结果才是真正移位的位数。例如,a>>33和a>>1的结果完全一样,而a>>32的结果和a相同。
对于long类型的整数移位a>>b,当b>64时,总是先用b对64求余(因为long类型是64位),得到的结果才是真正移位的位数。
注意:
当进行移位运算时,只要被移位的二进制码没有发生有效位的数字丢失(对于正数而言,通常指被移出的位全部都是0),不难发现左移n位就相当于乘于2的n次方,右移n位则是除以2的n次方。不仅如此,进行移位运算不会改变操作数本身,只是得到了一个新的运算结果,而原来的操作数本身是不会改变的。
x<
x>>n: x÷2ⁿ
逻辑运算符
逻辑运算符主要有如下6个:
&&:与,前后两个操作数必须都是true才返回true,否则返回false。
&:不短路与,作用于&&相同,但不会短路。
||:或,只要两个操作数中有一个是true,就可以返回true,否则返回false。
|:不短路或,作用于||相同,但不会短路。
!:非,只需要一个操作数,如果操作数为true,则返回false;如果操作数为false,则返回true。
^:异或,当两个操作数不同时才返回true,如果两个操作数相同则返回false。
短路: 在Java编程中,对于&&和||,Java只看第一个操作数的值,对于&&,如果第一个值为false,那么第二个不再计算,如果第一个值为true,那么还要计算第二个值;对于||,如果第一个值为false,那么还要计算第二个值,如果第一个值为true,那么第二个值不再计算;对于&和|操作符,两边无论如何都计算。
static的真正作用就是用于区分成员变量、方法、内部类、初始化块这四种成员到底属于类本身还是属于实例。在类中定义的成员,static相当于一个标志,有static修饰的成员属于类本身,没有static修饰的成员属于该类的实例。
static修饰的方法和成员变量,既可以通过类来调用,也可以通过实例来调用;没有使用static修饰的普通方法和成员变量,只可通过实例来调用。
向上转型和向下转型
向上转型也叫自动类型转换,构成多态。
父类类型 引用名称 = new 子类 类名();
当是多态时,该引用名称只能访问父类中的方法和属性,但是优先访问子类重写以后的方法。
向下转型
当是多态时,并且访问子类独有的属性或方法时,则必须进行向下转型。
当进行向下转型时,建议先进行判断,当合法则再转换为对应的类型,使用instanceof关键字,否则可能会出现类型转换异常java.lang.ClassCastException。
对象的this引用
Java提供了一个this关键字,this关键字总是指向调用该方法的对象。根据this出现位置的不同,this作为对象的默认引用有两种情形。
构造器中引用该构造器正在初始化的对象。
在方法中引用该方法的对象。
this关键字最大的作用就是让类中的一个方法,访问该类里的另一个方法或实例变量。
在现实世界里,对象的一个方法依赖于另一个方法的情形如此常见:例如,吃饭方法依赖于拿筷子方法,写程序方法依赖于敲键盘方法,这种依赖都是同一个对象两个方法之间的依赖。因此,Java允许对象的一个成员直接调用另一个成员,可以省略this前缀。
大部分时候,一个方法访问该类中定义的其他方法、成员变量时加不加this前缀的效果是完全一样的。
对于static修饰的方法而言,则可以使用类来直接调用该方法,如果在static修饰的方法中引用this关键字,则这个关键字就无法指向合适的对象。所以,static修饰的方法中不能使用this引用。由于static修饰的方法不能使用this引用,所以static修饰的方法不能访问不使用static修饰的普通成员,因此Java语法规定:静态成员不能直接访问非静态成员。
注意:
Java有一个让人极易“混淆”的语法,它允许使用对象来调用static修饰的成员变量、方法,但实际上这是不应该的。static修饰的成员属于类本身,而不属于该类的实例,既然static修饰的成员完全不属于该类的实例,那么就不应该允许使用实例去调用static修饰的成员变量和方法!所以请牢记一点:Java编程时不要使用对象去调用static修饰的成员变量、方法,而是应该使用类去调用static修饰的成员变量和方法!如果在其他Java代码中看到对象调用static修饰的成员变量、方法的情形,则完全可以把这种用法当成假象,将其替换成用类来调用static修饰的成员变量、方法的代码。
大部分时候,普通方法访问其他方法、成员变量时无须使用this前缀,但如果方法里有个局部变量和成员变量同名,但程序又需要在该方法里访问这个被覆盖的成员变量,则必须使用this前缀。
public class Dog {
// 定义一个jump()方法
public void jump() {
System.out.println("正在执行jump方法");
}
// 定义一个run()方法,run()方法需要借助jump()方法
public void run() {
Dog d = new Dog();
d.jump();
System.out.println("正在执行run方法");
}
}
public class DogTest {
public static void main(String[] args) {
// 创建Dog对象
Dog dog = new Dog();
// 调用Dog对象的run()方法
dog.run();
}
}
public void run() {
// 使用this引用调用run()方法的对象
this.jump();
System.out.println("正在执行run方法");
}
public void run() {
jump();
System.out.println("正在执行run方法");
}
除此之外,this引用也可以用于构造器中作为默认引用,由于构造器是直接使用new关键字来调用而不是使用对象来调用的,所以this在构造器中代表该构造器正在初始化的对象。
如果在某个方法中把this作为返回值,则可以多次连续调用同一个方法,从而使代码更加简洁。但是,这种把this作为返回值的方法可能造成实际意义的模糊。
public class example {
public int age;
public example grow() {
age++;
// return this;返回调用该方法的对象
return this;
}
public static void main(String[] args) {
example e = new example();
// 可以连续调用同一个方法
e.grow().grow().grow().grow();
System.out.println("e的age成员变量值是:" + e.age);
}
}
Java只有一种传递方式:值传递。无论是基本类型参数传递或是引用类型传递。引用类型传递相当于是复制了一个遥控器,但控制的是同一个电视。
形参个数可变的方法
JDK1.5以后,Java允许定义形参个数可变的参数,从而允许为方法指定数量不确定的形参。如果在定义方法时,在最后一个形参的类型后增加三点(...),则表明该形参可以接受多个参数值,多个参数值被当成数组传入。即可变长参数。
注意:个数可变的形参只能处于形参列表的最后。一个方法中最多只能包含一个个数可变的形参。个数可变的形参本质就是一个数组类型的形参,因此调用包含个数可变形参的方法时,该个数可变的形参既可以传入多个参数,也可以传入一个数组。
方法重载
Java运行同一个类里定义多个同名方法,只要形参列表不同就行。如果同一个类中包含了两个或两个以上方法的方法名相同,但形参列表不同,则被称为方法重载。
从上面介绍可以看出,在Java程序中确定一个方法需要三个要素。
调用者,也就是方法的所属者,既可以是类,也可以是对象。
方法名,方法的标识。
形参列表,当调用方法时,系统将会根据传入的实参列表匹配。
方法重载的要求就是两同一不同:同一个类中方法名相同,参数列表不同。至于方法的其他部分,如方法的返回值类型、修饰符等,与方法重载没有任何关系。
成员变量和局部变量
成员变量指的是在类里定义的变量,也就是field;局部变量指的是在方法里定义的变量。
成员变量无须显式初始化,只要为一个类定义了类变量或实例变量,系统就会在这个类的准备阶段或创建该类的实例时进行默认初始化,成员变量默认初始化时的赋值规则与数组动态初始化数组元素的赋值规则完全相同。并且,类变量的作用域比实例变量的作用域更大:实例变量随实例的存在而存在,而类变量则随类的存在而存在。实例也可以访问类变量,同一个类的所有实例访问类变量时,实际上访问的是该类本身的同一个变量,也就是说,访问了同一片内存区。
局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。这意味着,定义局部变量后,系统并未为这个变量分配内存空间,直到等到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中。
与成员变量不同,局部变量不属于任何类或者实例,因此它总是保存在其所在方法的栈内存中。如果局部变量是基本类型,则直接把这个变量的值保存在该变量对应的内存中;如果局部变量是一个引用类型的变量,则这个变量里存放的是地址,通过该地址引用到该变量实际引用的对象或数组。
关于控制符的使用,存在如下几条基本原则:
类里的绝大部分成员变量都应该使用private修饰,只有一些static修饰的、类似全局变量的成员变量,才可能考虑使用public修饰。除此之外,有些方法只用于辅助实现该类的其他方法,这些方法被称为工具方法,工具方法也应该使用private修饰。
如果某个类主要用于做其他类的父类,该类里包含的大部分方法可能仅希望被其子类重写,而不想被外界直接调用,则应该使用protected修饰这些方法。
希望暴露出来给其他自由调用的方法应该使用public修饰。因此,类的构造器通过使用public修饰,从而允许在其他地方创建该类的实例。因为外部类通常都希望被其他类自由使用,所以大部分外部类都使用public修饰。
构造器重载
为了在构造器B中调用构造器A中的初始化代码,又不会重新创建一个Java对象,可以使用this关键字来调用相应的构造器。当使用本类构造方法时,this()必须放在第一行。
创建任何对象总是从该类所在继承树最顶层类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器。如果某个父类通过this调用同类中重载的构造器,就会依次执行此父类的多个构造器。
向上转型:当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态。与方法不同的是,对象的实例变量则不具备多态性。
注意:
引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法。因此,编写Java代码时,引用变量只能调用声明该变量时所用类里包含的方法。例如,通过Object p = new Person();代码定义一个变量p,则这个p只能调用Object类的方法,而不能调用Person类里定义的方法。实例变量也是一样。
instanceof运算符
instanceof运算符的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是接口,可以把接口理解成一种特殊的类),它用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是,则返回true,否则返回false。
在使用instanceof运算符时需要注意:instanceof运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误。
初始化块
初始化块里的代码可以包含任何可执行语句,包括定义局部变量、调用其他对象的方法,以及使用分支、循环语句等。
初始化块虽然也是Java类的一种成员,但它没有名字,也就没有标识,因此无法通过类、对象来调用初始化块。初始化块只在创建Java对象时隐式执行,而且在执行构造器之前执行。
从某种程度上来看,初始化块是构造器的补充,初始化块总是在构造器执行之前执行。系统同样可以使用初始化块来进行对象的初始化操作。
与构造器不同的是,初始化块是一段固定执行的代码,它不能接收任何参数。因此初始化块对同一个类的所有对象所进行的初始化处理完全相同。基于此,不难发现初始化块的基本用法,如果有一段初始化处理代码对所有对象完全相同,且无需接收任何参数,就可以把这段初始化处理代码提取到初始化块中。
创建一个对象时,执行顺序:
执行java.lang.Object类的初始化块 ---> 执行java.lang.Object类的构造器 ---> 父类的初始化块 ---> 父类构造器 ---> 该类初始化块 ---> 该类构造器
静态初始化块
如果定义初始化块时使用了static修饰符,则这个初始化块就变成了静态初始化块,也被称为类初始化块(普通初始化块负责对对象执行初始化,类初始化块则负责对类进行初始化)。静态初始化是类相关的,系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行。因此静态初始化块总是比普通初始化块先执行。
静态初始化块也被称为类初始化块,也属于类的静态成员,同样需要遵循静态成员不能访问非静态成员的规则,因此静态初始化块不能访问非静态成员,包括不能访问实例变量和实例方法。
Java中的 == ,equals的区别和联系
==
如果变量时基本类型变量,且都是数值类型(不一定要求数值类型严格相同),则只要两个变量的值相等,就返回true。
如果是引用类型变量,只有它们指向同一个对象时,==判断才会返回true。==不可用于比较类型上没有父子关系的两个对象。
常量池:专门用于管理在编译时被确定并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口中的常量,还包括字符串常量。
equals
equals()方法是Object类提供的一个实例方法,因此所有引用变量都可以调用该方法来判断是否与其他引用变量相等。但使用这个方法判断两个对象相等的标准与使用==运算符没有区别,同样要求两个引用变量指向同一个对象才会返回true。因此这个Object类提供的equals()方法没有太大的实际意义,如果希望采用自定义的相等标准,则可采用重写equals方法来实现。
很多说法经常说equals()方法是判断两个对象的值相等。这个说法并不准确,什么叫对象的值呢?对象的值如何相等?实际上,重写equals()方法就是提供自定义的相等标准,你认为怎样是相等,那就怎样是相等,一切都是你做主!在极端情况下,你可以让Person对象和Dog对象相等!
通常而言,对任意重写equals()方法应该满足下列条件:
自反性:对任意x,x.equals(x)一定返回true。
对称性:对任意x和y,如果y.equals(x)返回true,则x.equals(y)也返回true。
传递性:对任意x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)一定放回true。
一致性:对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一致,那么一致是true,那么一致是false。
对任何不是null的x,x.equals(null)一定返回false。
对于static关键字而言,有一条非常重要的规则:类成员(包括方法、初始化块、内部类和枚举类)不能访问实例成员(包括成员变量、方法、初始化块、内部类和枚举类)。因为类成员是属于类的,类成员的作用域比实例成员的作用域更大,完全可能出现类成员已经初始化完成,但实例成员还不曾初始化的情况,如果允许类成员访问实例成员将会引起大量错误。
final成员变量
final修饰的成员变量必须由程序员显式地指定初始值。
归纳起来,final修饰的类变量、实例变量能指定初始值的地方如下:
类变量:必须在静态初始化块中指定初始值或声明该类变量时指定初始值,而且只能在两个地方的其中之一指定。
实例变量:必须在非静态初始化块、声明该实例变量或构造器中指定初始值,而且只能在三个地方的其中之一指定。
实例变量不能在静态初始化块中指定初始值,因为静态初始化块是静态成员,不可访问实例变量——非静态成员;类变量不能在普通初始化块中指定初始值,因为类变量在类初始化阶段已经被初始化了,普通初始化块不能对其重新赋值。
final int age;
{
pringtAge(); //此段代码是合法的,程序输出0
}
public void printAge(){
System.out.println(age);
}
注意:
final成员变量在显式初始化之前不能直接访问,但是可以通过方法来访问,基本上可以断定是Java设计的一个缺陷。按照正常逻辑,final成员变量在显式初始化之前是不应该允许被访问的。因此建议开发者尽量避免在final变量显式初始化之前访问它。
final局部变量
系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化。因此使用final修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。
如果final修饰的局部变量在定义时没有指定默认值,则可以在后面代码中对该final变量赋初始值,但只能一次,不能重复赋值;如果final修饰的局部变量在定义时已经指定默认值,则后面代码中不能再对该变量赋值。
final若修饰形参,则不能被赋值,由系统根据传入的参数来完成初始化。
final修饰基本类型变量和引用类型变量的区别
当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但是对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。
可执行“宏替换”的final变量
对一个final变量来说,不管它是类变量、实例变量,还是局部变量,只要该变量满足三个条件,这个final变量就不再是一个变量,而是相当于一个直接量。
使用final修饰符修饰。
在定义该final变量时指定了初始值。
该初始值可以在编译时就被确定下来。
只要不会被编译器内联优化的 final 属性就可以通过反射有效的进行修改 -- 修改后代码中可使用到新的值
final 属性,只要不是基本类型和字面 String,就可以正常使用反射修改它的值。以前曾为了想反射修改某个 final 值还特意把 final 关键字给去掉,完全不用这么做。所以对于基本类型和字面 String, final 只是预示着它的值不能被正常的代码修改。
注意:
final修饰符的一个重要用途就是定义“宏变量”。当定义final变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个final变量本质上就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。
final方法
final修饰的方法不可被重写,如果处于某些原因,不希望子类重写父类的某个方法,则可以使用final修饰该方法。
final类
final修饰的类不可以有子类,为了保证某个类不可被继承,则可使用final修饰这个类。
不可变类
不可变(immutable)类的意思是创建该类的实例后,该实例的实例变量是不可改变的。Java提供的8个包装类和java.lang.String类都是不可变类。
如果需要创建自定义的不可变类,可遵守如下规则:
使用private和final修饰符来修饰该类的成员变量。
提供带参数构造器,用于根据传入参数来初始化类里的成员变量。
仅为该类的成员变量提供getter方法,不要为该类的成员变量提供getter方法,因为普通方法无法修改final修饰的成员变量。
如果有必要,重写Object的hashCode()和equals()方法。equals()方法根据关键成员变量来作为两个对象是否相等的标准,除此之外,还应该保证两个用equals()方法判断为相等的对象的hashCode()也相等。
抽象方法和抽象类
抽象方法和抽象类必须使用abstract修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法。
抽象方法和抽象类的规则如下:
抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。
抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。即使抽象类里不包含抽象方法,这个抽象类也不能创建实例。
抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接口、枚举)5中成分。抽象类的构造器不能用于创建实例,主要是用于被其子类使用。
含有抽象方法的类只能被定义成抽象类。包括直接定义了一个抽象方法;或继承了一个抽象父类,但没有完全实现父类包含的抽象方法;或实现了一个接口,但没有完全实现接口包含的抽象方法三种情况。
注意:
归纳起来,抽象类可用“有得有失”4个字来描述。“得”指的是抽象类多了一个能力:抽象类可以包含抽象方法;“失”指的是抽象类失去了一个能力:抽象类不能用于创建实例。
abstract不能用于修饰成员变量,不能用于修饰局部变量,即没有抽象变量、没有抽象成员变量等说法;abstract也不能用于修饰构造器,没有抽象构造器,抽象类里定义的构造器只能是普通构造器。子类可以使用super关键字调用抽象父类的构造器。
static和abstract并不是绝对互斥的,static和abstract虽然不能同时修饰某个方法,但它们可以同时修饰内部类。
abstract关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此abstract方法不能定义为private访问权限,即private和abstract不能同时修饰方法。
接口
基本语法:
对上面语法的详细说明如下:
修饰符可以是public或者省略,如果省略了public访问控制符,则默认采用包访问权限访问控制符,即只有在相同包结构下才可以访问该接口。
接口名与类名采用相同的命名规则,即如果仅从语法角度来看,接口名只要是合法的标识符即可;如果要遵循Java可读性规范,则接口名应由多个有意义的单词连缀而成,每个单词首字母大写,单词与单词之间无须任何分隔符。接口名通常能够使用形容词。
一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。
接口里定义的是多个类共同的公共行为规范,因此接口里的常量、方法、内部类个内部枚举都是public访问权限。定义接口成员时,可以省略访问控制修饰符,如果指定访问控制修饰符,则只能使用public访问控制修饰符。
Java9为接口增加了一种新的私有方法,其实私有方法的主要作用就是作为工具方法,为接口中的默认方法或类方法提供支持。私有方法可以拥有方法体,但私有方法不能使用default修饰。私有方法可以是static修饰,也就是说,私有方法既可以是类方法,也可以是实例方法。
对于接口里定义的静态常量而言,它们是接口相关的,因此系统会自动为这些成员变量增加static和final两个修饰符。也就是说,在接口中定义成员变量时,不管是否使用public static final修饰符,接口里的成员变量总是使用这三个修饰符来修饰。而且接口里没有构造器和初始化块,因此接口里定义的成员变量只能在定义时指定默认值。
Java8允许在接口中定义默认方法,默认方法必须使用default修饰,该方法不能使用static修饰。由于默认方法并没有static修饰,因此不能直接使用接口来调用默认方法,需要使用接口的实现类的实例来调用这些默认方法。
接口的默认方法其实就是实例方法,但由于早期Java的设计是:接口中的实例方法不能有方法体;Java8也不能直接“推倒”以前的规则,因此只好重定义一个所谓的“默认方法”,默认方法就是有方法体的实例方法。
Java8允许在接口中定义类方法,类方法必须使用static修饰,该方法不能使用default修饰,无论程序是否指定,类方法总是使用public修饰——如果开发者没有指定public,系统会自动为类方法添加public修饰 。类方法可以直接使用接口来调用。
Java9增加了带方法体的私有方法,这也是Java8埋下的伏笔:Java8允许在接口中定义带方法体的默认方法和类方法——这也势必会引发一个问题,当两个默认方法(或类方法)中包含一段相同的实现逻辑时,程序必然考虑将这段实现逻辑抽取成工具方法,而工具方法是应该被隐蔽的,这就是Java9增加私有方法的必然性。
default关键字是Java8引入的,用于在接口中定义默认方法时使用,使用此关键字修饰可有方法体。
接口的继承
接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。和类继承相似,子接口扩展某个父接口,将会获得父接口里定义的所有抽象方法、常量。
接口和抽象类
接口和抽象类很像,它们都具有如下特征。
接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
接口和抽象类都可以包含抽象方法,实现接口或继承类的普通子类都必须实现这些抽象方法。
但接口和抽象类直接的差别非常大,这种差别主要体现在二者设计目的上。下面具体分析二者的差别。
接口作为系统和外界交互的窗口,接口体现的是一种规范。对于接口的实现而言,接口规定了实现者必须向外提供哪些服务(以方法的形式来提供);对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务(就是如何调用方法)。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。
从某种程度上来看,接口类似于整个系统的“总纲”,它制定了系统各模块应该遵循的标准,因此一个系统中的接口不应该经常改变。一旦接口被改变,对整个系统甚至其他系统的影响将是辐射式的,导致系统中大部分类都需要改写。
抽象类则不一样,抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能(那些已经提供实现的方法),但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同方式。
除此之外,接口和抽象类在用法上也存在如下差别:
接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则完全可以包含普通方法。
接口里只能定义静态常量,不能定义普通成员变量;抽象类里则可以定义普通成员变量,也可以定义静态常量。
接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
接口里不能包含初始化块;但抽象类则完全可以包含初始化块。
一个类最多只能有一个直接父类,包括抽象类;但一个类可以是直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。
内部类
内部类主要有如下作用:
内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
匿名内部类适合用于创建那些仅需要一次使用的类。
从语法角度来看,定义内部类与定义外部类的语法大致相同,内部类除需要定义在其他类里面之外,还存在如下两点区别:
内部类比外部类可以多使用三个修饰符:private、protected、static——外部类不可以使用这三个修饰符。
非静态内部类不能拥有静态成员。
非静态内部类
大部分时候,内部类都被作为成员内部类定义,而不是作为局部内部类。成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员;局部内部类和匿名内部类则不是类成员。
成员内部类分为两种:静态内部类和非静态内部类,使用static修饰的成员内部类是静态内部类,没有使用static修饰的成员内部类是非静态内部类。
注意:
外部类的上一级程序单元是包,所以它只有2个作用域;同一个包内和任何位置。因此只需2种访问权限:包访问权限和公开访问权限,正好对应省略访问控制符和public访问控制符。而内部类的上一级单元是外部类,它就具有4个作用域:同一个类、同一个包、父子类和任何位置,因此可以使用4种访问控制权限。
注意:
非静态内部类里不能有静态方法、静态成员变量、静态初始化块。但可以包含普通初始化块,作用于外部类普通初始化块作用完全相同。
静态内部类
如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此使用static修饰的内部类被称为类内部类,有的地方也称为静态内部类。
静态内部类可以包含静态成员,也可以包含非静态成员。
静态内部类是外部类的一个静态成员,因此外部类的所有方法、所有初始化块中可以使用静态内部类来定义变量、创建对象等。
外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问内部类的实例成员。
下面程序示范了这条规则:
public class StaticOuter {
static class Inner{
private static int prop = 88;
private int prop1 = 99;
}
public void accessInnerProp() {
//通过类名访问静态内部类的类成员
System.out.println(Inner.prop);
//通过实例访问静态内部类的实例成员
System.out.println(new Inner().prop1);
}
}
除此之外,Java还允许在接口中定义内部类,接口里定义的内部类默认使用public static修饰,也就是说,接口内部类只能是静态内部类!
使用内部类
1.在外部类内部使用内部类
在外部类内部使用内部类时,与平常使用普通类没有太大的区别。一样可以直接通过内部类类名来定义变量,通过new调用内部类构造器来创建实例。
唯一的区别在于:不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员。
在外部类内部定义内部类的子类与平常定义子类也没有太大区别。
2.在外部类以外使用非静态内部类
如果希望在外部类以外的地方访问内部类(包括静态和非静态两种),则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用。对于使用其他访问控制符修饰的内部类,则能在访问控制符对应的访问权限内使用。
在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:
OuterClass.InnerClass varName;
在外部类以外的地方创建非静态内部类实例的语法如下:
OuterInstance.new InnerConstructor();
注意:
非静态内部类的子类不一定是内部类,它可以是一个外部类。但非静态内部类的子类实例一样需要保留一个引用,该引用指向其父类所在外部类的对象。也就是说,如果有一个内部类子类的对象存在,则一定存在与之对用的外部类对象。
3.在外部类以外使用静态内部类
因为静态内部类是外部类类相关的,因此创建静态内部类对象时无须创建外部类对象。
在外部类以外的地方创建内部类实例的语法如下:
new OuterClass.InnerConstrutor();
不管是静态内部类还是非静态内部类,它们声明变量的语法完全一样。区别只是在创建内部类对象时,静态内部类只需使用外部类即可调用构造器,而非静态内部类必须使用外部类对象来调用构造器。
注意:
相比之下,使用静态内部类比使用非静态内部类要简单很多,只要把外部类当成静态内部类的包空间接口。因此当程序需要使用内部类时,应该优先考虑使用静态内部类。
局部内部类(作用域太小,鸡肋)
如果把一个内部类放在方法里定义,则这个内部类就是局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在外部类的方法以外的地方使用,因此局部内部类也不能访问控制符和static修饰符修饰。
注意:
对于局部成员而言,不管是局部变量还是局部内部类,它们的上一级程序单元都是方法,而不是类,使用static修饰它们没有任何意义。因此,所有的局部成员都不能使用static修饰。不仅如此,因为局部成员的作用域是所在方法,其他程序单元永远也不可能访问另一个方法中的局部成员,所以所有的局部成员都不能使用访问控制修饰符。
Java8改进的匿名内部类
匿名内部类适合创建那种只需要一次使用的类,创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类并不能重复使用。
new 实现接口() | 父类构造器(实参列表){
//匿名内部类的类体部分
}
从上面的定义可以看出,匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或实现一个接口。
关于匿名内部类还有如下两条规则:
匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。
匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。
当通过实现接口来创建匿名内部类时,匿名内部类也不能显式创建构造器,因此匿名内部类只有一个隐式的无参数构造器,故new接口后的括号里不能传入参数值。
但如果通过继承父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器,此处的相似指的是拥有相同的形参列表。
当创建匿名内部类时,必须实现接口或抽象父类里的所有抽象方法。如果有需要,也可以重写父类中的普通方法。
在Java8之前,Java要求被局部内部类、匿名内部类访问的局部变量必须使用final修饰,从Java8开始这个限制被取消了,Java8更加智能:如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了final修饰。
注意:
Java8将这个功能成为“effectively final”,它的意思是对于被匿名内部类访问的局部变量,可以用final修饰,也可以不用final修饰,但必须按照有final修饰的方式来用——也就是一次赋值后,以后不能重新赋值。