Java笔试准备重点拾遗2–OOP部分
1. 包
- 包的名字与路径有潜在的联系,包的根路径在CLASSPATH环境变量中被指定
- 如果引入的若干包中包含相同名称的类,则需要使用完整包路径来指定到底哪个类被使用了
- 可以使用不同的但具有相同接口的包来实现调试功能:比如一个包叫debug,里面的方法实现时会输出更多的信息;另一个包叫debugoff,里面相同的方法中只输出必要的信息
- 如果一个文件中没有明确指定包,jvm会认为其为默认包,它所在的目录就是它的包路径
2. 类成员、方法的访问控制
- 四种不同访问粒度:public > protected > (包访问权限,默认指定项,表现为不写) > private
- public:任何类都可以访问
- protected:当前类和继承它的类和与当前类在一个包中的类都可以访问
- 包访问权限:当前类和当前类在一个包中的类可以访问
- private:除了当前类谁都不能访问
- 如果对一个类的所有的构造方法使用private权限,将阻止这个类被继承
- 成员就算是设为private也不代表这个成员完全不能被修改,可以通过public方法(如set方法)修改成员的值,某些情况下静态方法也有修改成员值得可能
3. 访问、修改类中一个成员的方法
- 将该成员设为public
- 对于同一个包中的类,可以选择不加访问修饰词
- 使用继承的方式访问父类的成员
- 类对该成员提供访问器与变异器方法(get/set方法),此方法最常用
4. 类的访问控制
- 除了内部类,其他类的只能是public和包访问权限,不能使private和protected的
- 每个文件中最多只能有一个public类;从某种程度上一个文件中可以不存在public类,不过并不常见,而且容易造成混乱难以维护
- public类的名称与文件名高度一致,包括大小写
5. 类的复用
- 类复用的方式只需将套用的类作为新类的成员即可
- 复用的对象在默认初始化的时候会赋予null
6. 手动初始化对象成员的4种方式
- 在定义成员的地方初始化
- 在类的构造器中
- 在即将使用这个对象的时候(惰性初始化)
- 使用实例初始化
7. 继承时的访问控制
- 如果基类方法是public的,子类必然可以访问
- 如果基类方法是protected,子类可以访问
- 如果基类方法是包访问权限,如果子类与父类在一个包中,则可以访问;不在一个包中则无法访问
- 如果基类方法是private,子类一定无法访问
- 如果子类重载了父类的方法,子类需要使用super来指定父类对象
8. 继承中的构造器
- 子类的构造方法只能初始化子类新添加的成员,不能对父类的成员直接初始化。需要使用父类的构造函数(用super()代替),如果有参数需要将参数传进去。
- 调用父类的构造函数必需放在子类构造器的第1句,否则会报错。
- 如果不显示地调用父类构造器,JVM会尝试使用
super()
还调用父类的默认构造函数。如果找不到合适的构造函数,则报错。
9. 代理
- java本身不支持代理
- 可以通过组合+暴露接口的方式实现代理
10. 子类重载父类方法
如果子类需要覆盖父类的方法时,在方法前使用@Override
注解。
11. final
final数据
final数据可以表示:
1. 编译时常量(只能是基本数据类型)
2. 在运行初始化时赋予的值,并且在运行时不再改变
如果数据类型是基本数据类型,final即限制这个数值不会发生变化。如果这个类型是对象(引用类型),它指的是对象的地址不发生变化。也就是说,对象所占的位置不能变化,但是对象内部的值可以变化。
编译器常量可以提升一定的执行效率,因为在编译时已经确定了存储空间。
如果final成员在声明时并没有进行初始化,则可以在构造函数中“动态”初始化。
final参数
final参数表示该参数的值在方法中无法被改变:基本数据类型指的是值本身;引用类型指的是引用值本身,也即不能改变变量所指向的对象。
final方法
对方法使用final有两种情况:
1. 锁定该方法,使得任何继承它的类都不能改写此方法
2. 在早期版本中可以提高运行效率,减少切换代码的步骤(现已不推荐)
final与private对方法使用
一个类中,所有的private方法都隐式地声明为final,因为任何private方法都无法在子类中被调用,也就无法在子类中override这个方法。final与private允许组合使用,但是final并不会被表现。
如果在子类中“override”一个private方法,是无效的。但是如果这么做了,是可以通过编译的。因为这相当于在子类中重新建立了一个private方法,并赋予含义。而父类的private方法,并没有通过此方法被覆盖。
final类
一个final类是无法被继承的。它的所有成员与方法都隐式地被指定为final。
12. 类的初始化与加载
- 加载对应的类,并判断该类是否存在基类;如果存在基类,则继续寻找下一个基类,直到找不到基类。(这里可以简单地理解为找到的类不是extends另一个类。其实所有的类都是继承于Object类,只不过Object类在初始化时没有任何操作。)
-
则从根基类开始,逆序初始化所有类的静态成员。对于初始化对应的每一个类,如果它已经加载过,则不再加载静态成员,否则加载静态成员。
-
创建对象,将对象成员设为默认初始值。然后溯源而上,依次完成指定初始化与基类的构造方法。
总结:父类静态 > 子类静态 > 父类指定 > 父类构造 > 子类指定 > 子类构造
在静态与指定中,内部顺序依声明的顺序。
class Helper {
static int print(String str) {
System.out.println(str);
return 1;
}
}
class A {
private int a = Helper.print("A.a");
public A(int a) {
this.a = a;
System.out.println("A.A()");
}
static public int b = Helper.print("A.b");
static {
b = 20;
System.out.println("A.static{}");
}
{
a = 3;
System.out.println("A.{}");
}
}
class B extends A {
private int a = Helper.print("B.a");
public B(int a) {
super(a);
this.a = a;
System.out.println("B.A()");
}
static public int b = Helper.print("B.b");
static {
b = 10;
System.out.println("B.static{}");
}
{
a = 8;
System.out.println("B.{}");
}
}
public class ClassLoad {
public static void main(String[] s) {
B b = new B(5);
System.out.println(B.b);
}
}
A.b // 基类静态,b的指定初始化在前面
A.static{} // 基类静态,static块初始化在后面
B.b // 子类静态,b的指定初始化在前面
B.static{} // 子类静态,static块初始化在后面
A.a // 父类普通成员,指定初始化
A.{} // 父类普通初始化块
A.A() // 父类构造方法
B.a // 子类普通成员,指定初始化
B.{} // 子类普通初始化块
B.A() // 子类构造方法
10
13. 方法调用绑定来解决多态的问题
当使用多态的时候,我们通常会向上转型为其父类类型。但是当调用方法的时候,如何知道调用的方法应该是哪个导出类的呢?这就需要动态绑定技术。动态绑定简言之是在运行时判断对象的类型,从而调用恰当的方法。除了static和final方法外(private属于final),其余方法都是动态绑定的。
属性/方法重名造成的无法多态
如果父类和子类都可被访问,且对于某个成员,子类和父类的名字相同。这就意味着子类的实现会覆盖掉父类的实现,父类的同名成员/方法便无法直接调用。
静态成员和方法不具有多态性
14. 在构造器中应该调用基类final(private)或static方法
在构造函数中,不要指望多态发挥作用。因为一个对象在加载的时候,内存会首先清空为二进制0.然后按照对象初始化的顺序进行初始化。这就意味着,如果在父类的构造方法中调用子类的某个方法,并不一定可以得到正确的结果。因为子类的构造还没有开始,内存中只是一片空。
15. 协变返回类型
同一个方法,在导出类中的返回值类型可以是在基类中的返回值类型的导出类型。
听上去像绕口令,其实它说的是:
我们有:class A{}, class AResult{}, class B extends A {}, class BResult extends AResult{}
如果,A中有一个方法f(),它的返回值是AResult,那么B中的覆盖A中的f(),它的返回值可以是BResult(AResult的派生类)
16. 向上转型与向下转型
向上转型为多态的实现创造了可能
向上转型指的是一个导出类类型作为父类类型进行处理。这样父类出现的地方其子类也可以出现。
向下转型是受到制约的
在实现多态的时候,父类中的一个方法究竟是按照父类的定义执行还是按照子类的定义执行是取决于对象本身的。如果对象是父类对象,则使用父类的方法;如果是子类对象,则使用子类的方法,这一过程即使是没有使用强制类型转换,也是完成了一次向下转型。不过向下转型并不是怎么指定就可以怎么转换的。Java为每个对象都创建了一些信息以记录该对象如何被创建(该信息称为运行时类型信息,RTTI)。对于非法的转换,jvm会抛出ClassCastException。
这种非法的转换可能是错误转换,如B和C都是继承于A类,对于B类的对象在多态时以A类对象出现,然后转成了C类对象;还有可能是A类的对象,调用了继承于A类的B类对象的新增方法。也就意味着调用了这个对象本身不存在的方法。
17. 接口与抽象类
抽象类是含有抽象方法的类(只要有一个抽象方法就是抽象类),使用abstract修饰。抽象方法是一种特殊的方法,这个方法只是声明了“长相”,但是没有实现。抽象类的目的是声明一个共性的抽象的模板,由子类完成方法的具体实现。抽象类是无法被实例化的,因为存在未实现的方法。
如果一个类继承了抽象类,且没有完全实现里面的方法,这个类依然是抽象类,需要显式地用abstract修饰。
接口是一个完全抽象的抽象类,它内部只有抽象方法。一个接口可以理解为一系列操作的抽象(比如:叫早可以作为一个接口被抽象,它内含一个通用的方法是响铃。如果手机实现了叫早接口,那么手机要赋予这个叫早功能具体的实现;如果电视机也实现了叫早接口,那么电视机就要赋予这个功能具体的实现。并且,很显然,手机与电视机在叫早上的实现是不同的。),也可以按照组合的方式理解(每个接口规定了一个功能,一个类可以实现多个接口,提供方法的具体实现来完成实现的功能。从宏观上看,这些被实现的接口都是那个集成它们的类的“父类”。这个特性在多重继承与工厂模式中有应用)。与抽象类相同,一个接口不能被实例化,只有完全实现接口中的方法的类才可以被实例化。
非抽象类实现接口时,必需实现接口声明的所有方法。抽象类可以保留部分接口方法不予实现。接口默认被abstract、public修饰。
接口与抽象类的不同点:
– 接口中的方法必须全部是抽象的;接口中的成员一定是final、static(、public,接口默认情况)的并且不接受只final声明但未赋予初始值的成员(必须是指定初始化方式声明的)
– 抽象类的方法允许有不抽象的方法;抽象类中的成员没有任何限制
18. 完全解耦
不知所云
19. 多重继承
Java不允许多重继承,实现多重继承只能通过实现不同的接口完成。
当继承与接口同时出现时,需要先写继承,后写接口。当一个方法的参数中有一个接口,那么实现这个接口的所有类的对象都可以作为参数传入这个方法。从表征上看,一个接口作为参数是没有意义的,因为不可能有这个接口对应的对象。但是在java中,使用多态的想法来考虑,这件事便是可行的。接口看作类的“父类”,当类对象传入时,实际上是进行了一次向上转换,在运行时绑定的方法依然是这个对象实现的方法。
例:
interface IMultiInherit {
int process();
}
class CMultiInherit implements IMultiInherit {
@Override
public int process() {
return 0;
}
}
public class MultiInherit {
public static int p(IMultiInherit x) {
return x.process();
}
public static void main(String[] s) {
CMultiInherit c = new CMultiInherit();
System.out.println(p(c)); // IMultiInherit接口的CMultiInherit实现
}
}
20. 接口的继承、组合与嵌套
接口可以继承接口,也可以继承多个接口,使用extends关键词。类不能继承接口,接口也不能继承类。
接口中可以出现同名方法,但是最好可以区分,如方法的实现相同,否则会出现错误。
接口与类可以嵌套。接口可以嵌套接口但是不能嵌套类,嵌套的接口必须是public的,private、protected都不可以。类可以嵌套接口,也可以嵌套类,当然也可以嵌套实现了嵌套接口的嵌套类。类中可以存在私有借口,但是这个接口只能在类中被处理,不论是被实现还是调用接口的方法。
Java编程思想第4版:P185-186
21. 接口中的成员
接口中的成员默认为static、public、final的。因此这个方法多用于枚举类型的表达(在早期较常见)。这些成员会存放在接口的静态存储区中。由于这些成员是静态的,它们在接口第一次加载的时候被初始化。
22. 工厂方法模式
通常工厂方法模式用于具有特征的一系列类中(就是实现了相同接口的不同类之间)。它的好处是可以通过同样的调用方法,通过不同类型的参数来产生不同类型的对象。从而实现创建对象与对象类型的解耦。
例子:
interface Service {
void do1();
void do2();
}
interface ServiceFactory {
Service getService();
}
class Service1 implements Service {
void do1() {System.out.println("Service1.do1()");}
void do2() {System.out.println("Service1.do2()");}
}
class Service1Factory implements ServiceFactory {
Service getService() { return new Service1();}
}
class Service2 implements Service {
void do1() {System.out.println("Service2.do1()");}
void do2() {System.out.println("Service2.do2()");}
}
class Service2Factory implements ServiceFactory {
Service getService() { return new Service2();}
}
public class Factory {
public static Service getService(ServiceFactory factory) { return factory.getService();}
public static void main(String[] s) {
Service s1 = getService(new Service1Factory());
Service s2 = getService(new Service2Factory());
}
}
在这个例子中,Service是一个需要实现的接口,Service1和Service2是它的两个实现。为了创建Service1和Service2对象,我们并不是直接去new,而是使用工厂来处理。为每个类实现一个工厂类,这个类实现工厂接口,可以创建对应的实体对象。最后实现一个通用方法(这里是Factory.getService()),接受不同的工厂类的对象作为参数。这样可以根据参数的不同而创建不同的对象(创建的方式是调用这个工厂对象的getService())。
这种方式的优势是可以利用多态性完成创建对象的选择:如果有了新的类Service3,只需额外实现一个Service3Factory类(此工厂类实现ServiceFactory接口)即可。不过我们需要为每个类创建一个对应的工厂方法,这点可以利用后续的反射来解决。
工厂模式代码可以通过匿名内部类的使用来进行优化。
23. 内部类与匿名内部类
在类中声明的类就是内部类。内部类可以继承于其他类或者是实现任何方法,和普通类没有区别。内部类的方法可以访问外围类的全部成员与方法。如果需要外围类的引用,则可以使用外部类的名字.this
。如果在类外创建一个内部类,需要先创建外围类的对象,然后使用外围类对象创建内部类。写法为外围类.内部类 对象名 = 外围类对象.new 内部类名();
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
内部类可以存在于任何一个大括号中,比如if语句。此时,这个类编译的顺序并不受实际执行效果的影响。(就算这部分执行不到,类也会被编译,只是不会被加载。)作用域中的内部类的生效区域收到该作用域的影响,也即在作用域外,这个类是无法使用的。普通内部类不能存在static成员与static方法,除非这个内部类是static的。
private内部类是无法被外界访问的,也就是说直接new对象是不允许的。不过可以通过一个public方法来返回内部类的对象。如果这个内部类实现了某个方法,我们可以向上转型,返回一个父类类型的对象。这样调用者连对象的真正类型都不知道,但是方法可以正确执行。这样可以隐藏代码实现细节。
匿名内部类本身是一种继承/实现的模式,它是继承类/实现接口的简化写法,并且自动向上转型为父类对象。
例:
public class A {
class Service1 implements Service {
private int i = 1;
public int getValue() { return i; }
}
public Service getService() { return new Service1(); }
}
这段代码可以使用匿名内部类改写:
public class A {
public Service getService() {
return new Service() {
private int i = 1;
public int getValue() { return i; }
};
}
}
同样的改写方法可以应用到前述的工厂模式中:
将工厂方法的实现改写为在Service类中创建一个内部类,实现ServiceFactory接口的getService()方法,在方法中返回对象即可。
Java编程思想第4版:P199-200
24. 匿名内部类较普通类的限制
初始化
匿名内部类只能使用实例初始化(用{}括起来的初始化语句)。
new Service() {
{ Do initialization }
Other definitions
};
如果它的父类存在带有参数的构造器,在new父类的时候可以将参数传入: new Service(1) { ... };
。
既然匿名内部类只能使用实例初始化,那么内部类便不存在构造方法,也不会有重载构造方法。
使用外围类的对象
匿名内部类中如果要使用外围类的对象,需要将外围类的对象设为final的。如果只是将变量传给父类的构造器,而内部类没有直接使用这个对象则不受影响。匿名内部类实例初始化中用到的外围类对象需要用final修饰。
继承/实现
按照定义,匿名内部类只能继承一个类或实现一个接口。而且必须要有一个继承的类或实现的接口。普通内部类可以实现多个接口,也可以继承类+实现接口。
25. 嵌套类
普通多层嵌套
多层嵌套类中,内部类可以访问所有外围类的成员和方法。
静态嵌套
如果内部类声明为static,内部类对象与外围类对象则不存在联系。而且,内部类中方位外部类的那个this引用将不存在。
接口内部的类
由于接口是public和static的,它内部的类也是static的,即使不显式地声明。这种方式可以用于在接口中创建一个公共方法,而且这个公共方法会被这个接口的不同实现所共用。
26. 内部类的常见使用场景
实现多重继承
对于接口来说,这一点不那么重要,因为可以实现多个接口。
对于类来说,内部类可以方便地实现多重继承:首先外围类继承父类A,然后内部类继承父类B,由于外围类对内部类是透明的,因此内部类可以访问父类B的成员与方法,也可以访问父类A的成员与方法,也就实现了多重继承。(重名方法无法在这里解决。)
回调函数
更多例子在GUI中涉及
控制框架
外围类作为一个实体框架,其组成部分各自成为一个内部类,内部类共用的属性可以设为外围类的属性。
Java编程思想第4版:P207-211
27. 内部类的继承问题
继承内部类
内部类在继承时,子类的构造方法需要调用父类的构造方法。由于内部类本身没有构造方法,外围类的构造方法就是需要被调用的。又由于内部类并没有办法知道外围类对象(它自己都是被外围类对象创建的),因此我们需要手动地传一个外围类对象,来完成它自己的初始化。
Java编程思想第4版:P212
继承外围类
如果父类包括内部类,在继承外部类时,默认下这个内部类会被保留,就算子类中定义了一个相同的方法。
如果真的想继承外围类的同时覆盖掉父类的内部类,则需要在子类中重新定义一个内部类,这个类继承父类的内部类,在这个内部类中覆盖原先的方法。