Java基础2
主要内容来自华科-辜老师ppt
以下仅将个人觉得重要的内容摘取整理出来了,并补充了一点内容用以理解
一、对象和类
类(class)定义或封装同类对象共有的属性和方法,即将同类型对象共有的属性和行为抽象出来形成类的定义。
Java没有struct和union。
构造函数
无返回类型,名字同类名,用于初始化对象。
只在new时被自动执行。
必须是实例方法(无static),可为公有、保护、私有和包级权限。
如果类未定义任何构造函数,编译器会自动提供一个不带参数的默认构造函数。
Java没有析构函数,但垃圾自动回收之前会自动调用finalize( ),可以覆盖定义该函数(但是finalize调用时机程序员无法控制)。
访问对象:通过对象引用访问。JVM维护每个对象的引用计数器,只要引用计数器为0,该对象会由JVM自动回收。
在实例方法中有个this引用,代表当前对象(引用当前对象:相当于指针),因此在实例方法里,可以用this引用访问当前对象成员。
包package
包是一组相关的类和接口的集合。将类和接口分装在不同的包中,可以避免重名类的冲突,更有效地管理众多的类和接口。因此package就是C++里的namespace。
package语句必须出现在.java文件第一行,前面不能有注释行也不能有空白行,该.java文件里定义的所有内容(类、接口、枚举)都属于package所定义的包里。如果.java文件第一行没有package语句,则该文件定义的所有内容位于default包(缺省名字空间),但不推荐。
不同.java文件里的内容都可以属于同一个包,只要它们第一条package语句的包名相同。
如果要使用其它包里标识符,有二个办法:
- 用完全限定名,例如要调用java.util包里的Arrays类的sort方法: java.util.Arrays.sort(list);
- 在package语句后面,先引入要使用其它包里的标识符,再使用:
import java.util.Arrays; //或者: import java.util.*;
Arrays.sort(list);
按需类型导入(type import on demand):并非导入一个包里的所有类,只是按需导入
import java.util.*;
不是把包里的标识符都引入到当前.java文件,只是使包里名字都可见,使得我们要使用引入包里的名字时可以不用使用完全限定名,因此在当前.java文件里可以定义与引入包里同名的标识符。但二义性只有当名字被使用时才被检测到。
分析下面的错误
package p1;
public class A {
}
package p2;
import p1.*; //按需导入,没有马上把p1.A引入到当前域
//因此当前文件里可以定义A
public class A {
public static void main(String[] args){
A a1 = new A(); //这时A是p2.A
System.out.println(a1 instanceof p2.A); //true
//当前域已经定义了A,因此要想使用package p1里的A,(自己有了,肯定用自己的)
//只能用完全限定名
p1.A a2 = new p1.A();
}
}
package p1;
public class A {
}
package p2;
public class A {
}
package p3;
//可以按需导入,没有马上把p1.A,p2.A引入到当前域
//因此下面二个import不会保错
import p1.*;
import p2.*;//(不是立马导入,这时两个A还没导入,没有冲突)
public class B {
//当名字被使用时二义性才被检测
A a; //报错,Reference to A is a ambiguous, p1.A and p2.A match; (自己没有,去两个*里面找)
p1.A a1; //这时只能用完全限定名
p2.A a2;
}
package p3;
import p1.A;
import p2.A; //报错,p1.A is already defined in a single type import
public class B {
}
@Override可以不加,但是使用@Override注解有如下好处:
1:可以当注释用,方便阅读;
2:编译器可以给你验证@Override下面的方法名是否是父类中所有的,如果没有则报错。例如,如果没写@Override,而下面的方法名又写错了,这时你的编译器是可以编译通过的,因为编译器以为这个方法是你的子类中自己增加的方法。
Java注解为 Java 代码提供元数据。注解可以指示编译器做些额外的动作,甚至可以自定义Java注解让编译器执行自定义的动作。Java提供了Annotation API让我们自定义注解。
常量
常量通常定义为public。
final修饰实例方法时,表示该方法不能被子类覆盖(Override) 。非final实例方法可以被子类覆盖(见继承)
final修饰静态方法时,表示该方法不能被子类隐藏(Hiding)。非final静态方法可以被子类隐藏。
构造函数不能为final的。
构造函数是不能被继承的,也就不能被重写,final修饰实例方法主要是为了避免函数被重写,所以不用final修饰。
方法重载:同一个类中、或者父类子类中的多个方法具有相同的名字,但这些方法具有不同的参数列表(不含返回类型,即无法以返回类型作为方法重载的区分标准)
方法覆盖和方法隐藏:发生在父类和子类之间,前提是继承。子类中定义的方法与父类中的方法具有相同的方法名字、相同的参数列表、相同的返回类型(也允许子类中方法的返回类型是父类中方法返回类型的子类)
构造函数不能用static修饰,静态函数无this引用。
静态方法内部只能访问类的静态成员 (因为实例成员必须有实例才存在,当通过类名调用静态方法时,可能该类还没有一个实例)。
静态方法没有多态性。
类访问控制符:public和包级(默认);类的成员访问控制符:private、protected、public和包级(默认)
注意:类只有public和包级
Java继承时无继承控制(见继承,即都是公有继承,和C++不同),故父类成员继承到派生类时访问权限保持不变(除了私有)。
也就是说,Java不会根据 类的访问控制 去限制 类成员的访问控制
成员访问控制符的作用:
private: 只能被当前类定义的函数访问。
包级:无修饰符的成员,只能被同一包中的类访问。
protected:子类、同一包中的类的函数可以访问。
public: 所有类的函数都可以访问。
注意子类实例只能访问自己对应父类实例的protected成员,不能访问其他父类实例的protected成员。
访问控制针对的是类型而不是对象级别(小心)
public class Foo{
private boolean x;
public void m(){
Foo foo = new Foo();
//因为对象foo在Foo类内使用,所以可以访问私有成员x,并不是只能访问this.x
boolean b = foo.x //ok
}
}
有些特殊场合,可能会防止用户创建类的实例,这可以通过将构造函数声明为私有的来实现。
this引用指向调用某个方法的当前对象。
二、继承和多态
继承、子类和父类
如果class C1 extends C2,则称C1为子类(subclass),C2为父类(superclass)。
子类继承了父类中可访问的数据和方法,子类也可添加新的数据和方法,
子类不继承父类的构造函数。
一个类只能有一个直接父类(Java不支持多重继承,因为Java的设计者认为没有必要)。
Java的继承都是公有继承,因此被继承的就是父类,继承的类就是子类。因此父类的成员如果被继承到子类,访问权限不变。
说明:任何类在设计时应考虑覆盖祖先类Object的如下函数:equals,clone,toString等
父类的私有属性在子类中不可见(即不能在子类里直接访问)
但可以通过所继承的get和set方法设置和访问。
初始化块
初始化块是Java类中可以出现的第四种成员(前三种包括属性、方法、构造函数),分为实例初始化块和静态初始化块。
实例初始化模块(instance initialization block,IIB)是一个用大括号括住的语句块,直接嵌套于类体中,不在方法内。
**它的作用就像把它放在了类中每个构造方法的最开始位置,**用于初始化对象。
实例初始化块先于构造函数执行
作用:如果多个构造方法共享一段代码,并且每个构造方法不会调用其他构造方法,那么可以把这段公共代码放在初始化模块中。
一个类可以有多个初始化模块,模块按照在类中出现的顺序执行。
实例初始化模块还有个作用是可以截获异常
public class A{
//在实例初始化块里初始化数据成员可以截获异常
private InputStream fs = null;
{
try{ fs = new FileInputStream(new File(“C:\\1.txt”));}
catch(Exception e){ …}
}
public A(){ … }
}
实例初始化模块最重要的作用是当我们需要写一个内部匿名类时:匿名类不可能有构造函数,这时可以用实例初始化块来初始化数据成员。(这里同时复习匿名内部类)
interface ISay{ public abstract void sayHello(); }
public class InstanceInitializationBlockTest {
public static void main(String[] args){
ISay say = new ISay()
{ //这里定义了一个实现了ISay接口的匿名类
//final类型变量一般情况下必须马上初始化,一种例外是:final实例变量可以在构造函数里再初始化。
//但是匿名类又不可能有构造函数,因此只能利用实例初始化块
private final int j; //为了演示实例初始化块的作用,这里特意没有初始化常量j
{
j = 0; //在实例初始化块里初始化j
}
@Override
public void sayHello() { System.out.println("Hello");}
};
say.sayHello();
}
}
一个类可以有多个实例初始化块,对象被实例化时,模块按照在类中出现的顺序执行,构造函数最后运行。
静态初始化模块是由static修饰的初始化模块{},只能访问类的静态成员,并且在JVM的Class Loader将类装入内存时调用。(类的装入和类的实例化是两个不同步骤,首先是将类装入内存,然后再实例化类的对象)。
在类体里直接定义静态变量相当于静态初始化块
第一次使用类时装入类
- 如果父类没装入则首先装入父类,这是个递归的过程,直到继承链上所有祖先类全部装入
- 装入一个类时,类的静态数据成员和静态初始化模块按它们在类中出现的顺序执行
实例化类的对象
- 首先构造父类对象,这是个递归过程,直到继承链上所有祖先类的对象构造好
- 构造一个类的对象时,按在类中出现的顺序执行实例数据成员的初始化及实例初始化模块
- 执行构造函数函数体
super关键字(关注)
利用super可以显式调用父类的构造函数
super(parametersopt)调用父类的的构造函数。
必须是子类构造函数的第1条且仅1条语句(先构造父类)。
如果子类构造函数中没有显式地调用父类的构造函数,那么将自动调用父类不带参数的构造函数。(也就是说,如果没显式调用父类构造函数,编译器自动在第一行补上super() )
父类的构造函数在子类构造函数之前执行。
不能使用super.super.p()这样的super链
如果一个类自定义了构造函数(不管有无参数),编译器不会自动加上无参构造函数
如果一个类没定义任何构造函数,编译器会自动地加上无参构造函数。
编译器在为子类添加无参构造函数时,函数体里会用super( )默认调用父类的无参构造函数,如果找不到父类无参构造函数,则编译器为子类添加无参构造函数失败,编译报错。
如果一个类定义了带参数的构造函数,一定别忘了定义一个无参的构造函数,原因是:由于系统不会再自动加上无参构造函数,就造成该类没有无参构造函数
如果父类没有无参构造函数,那么子类构造函数里若调用父类无参构造函数就会编译出错。
覆盖和隐藏
如果子类重新定义了从父类中继承的实例方法,称为方法覆盖(method override)。
仅当父类方法在子类里是可访问的,该实例方法才能被子类覆盖,即父类私有实例方法不能被子类覆盖,父类实例私有方法自动视为final的。(final修饰的方法不能被重写)
静态方法不能被覆盖,如果静态方法在子类中重新定义,那么父类方法将被隐藏。
覆盖特性:一旦父类中的实例方法被子类覆盖,同时用父类型的引用变量引用了子类对象,这时不能通过这个父类型引用变量去访问被覆盖的父类方法(即这时被覆盖的父类方法不可再被发现)。因为实例方法具有多态性(晚期绑定)
在子类函数中可以使用super调用被覆盖的父类方法。
隐藏特性:指父类的变量(实例变量、静态变量)和静态方法在子类被重新定义,但由于类的变量(实例和静态)和静态方法没有多态性,因此通过父类型引用变量访问的一定是父类变量、静态方法(即被隐藏的可再发现)。
也就是说只有实例方法才具有多态性,静态方法不具有,静态方法早期绑定。
实例方法看运行时类型,静态方法看编译时类型。
方法覆盖的哲学涵义:子对象当然可以修改父类的行为(生物进化除了遗传,还有变异)
覆盖时可以改变访问控制,即可以更改protected为public等,仍然是覆盖。
绑定
绑定:找到函数入口地址的过程
引用变量o有二个类型:声明类型A,实际运行时类型B
多态性属于晚期绑定,静态属于早期绑定,重写(覆盖)属于晚期绑定,隐藏、重载等属于早期绑定。
(初学这部分很容易犯错,建议参考一些个人博客的其他java习题,由于篇幅,就不在这里放了)
Object中的方法
java.lang.Object类是所有类的祖先类。如果一个类在声明时没有指定父类,那么这个类的父类是Object类。
它提供方法如toString、equals、getClass、clone、finalize。前3个为公有,后2个为保护。getClass为final(用于泛型和反射机制,禁止覆盖)。
equals方法:用于测试两个对象是否相等。Object类的默认实现是比较两个对象引用是否引用同一个对象。(通常要重写覆盖)
toString方法:返回代表这个对象的字符串。Object类的默认实现是返回由类名、@和hashCode组成。
Object的toString方法提供的信息不是很有用。因此通常子类应该覆盖该方法,提供更有意义的信息
要实现一个类的clone方法,首先这个类需要实现Cloneable接口,否则会抛出CloneNotSupportedException异常。
还要公有覆盖clone方法,即Object类里clone方法是保护的,子类覆盖这个方法时应该提升为public。
方法里应实现深拷贝clone,Object的clone实现是浅拷贝(按成员赋值)。
如果对象的数据成员有引用类型,则对象的拷贝不能简单地按成员赋值;而是要保证引用类型的数据成员引用的是不同对象,但内容一样。因此需要覆盖Object的clone方法,方法的实现必须是深拷贝。
但是如果对象的数据成员为String及8种基本值类型对应的包装类类型,则这样的数据成员采用浅拷贝(按成员赋值就没有问题)。
原因:这些类型的对象的内容是不可更改的。如果把o2.s重新赋值为o2.s=“World”,实际是对o1.s没有任何影响,因为o2.s引用了另外的对象,和o1.s引用的对象不一样了。
public Object clone() throws CloneNotSupportedException {
// A newObj = new A(); //new一个新对象,该方法不好:在有继承关系的情况下,不利于复用父类的clone方法
A newObj = (A)super.clone(); //强烈建议这么做(不要重复造轮子,尽量去使用父类的clone)
newObj.values = this.values.clone(); //数组的clone是深拷贝,如果去掉clone,则是浅拷贝
return newObj;
}
多态性、动态绑定、类型转换
Class Student extends Person{ …}
Person p = new Student();//OK 父类引用可直接指向子类对象
Student s = new Person();//error,子类引用不能直接赋给父类引用
多态:通过引用变量调用实例函数时,根据所引用的实际对象的类型,执行该类型的相应实例方法,从而表现出不同的行为称为多态。通过继承时覆盖父类的实例方法实现多态。
多态实现的原理:在运行时根据引用变量指向对象的实际类型,重新计算调用方法的入口地址(晚期绑定)。
当调用实例方法时,由Java虚拟机动态地决定所调用的方法,称为动态绑定(dynamic binding)或者晚期绑定或者延迟绑定(lazy binding)或者多态。
程序还没运行,编译器无法知道p会指向什么对象,编译器在编译时只能根据变量p的声明类型(Person)来类型检查。
为了避免风险,最好用instanceof来做实例类型检查。
重载和多态的关系
重载发生在编译时(Compile time),编译时编译器根据实参比对重载方法的形参找到最合适的方法。
多态发生在运行(Run time)时,运行时JVM根据变量所引用的对象的真正类型来找到最合适的实例方法。
有的书上把重载叫做“编译时多态”,或者叫“早期绑定”(早期指编译时)。
多态是晚期绑定(晚期指运行时)
绑定是指找到函数的入口地址的过程。
对象的访问运算符.
优先与类型转换运算符。
((Circle)object).getArea() //OK
(Circle)object.getArea(); //错误
final可以修饰变量、方法、类
final修饰变量
- final成员变量:常量,数据初始化后不能再修改。
- final局部变量:常量,数据初始化后不能再修改。
final修饰方法(实例方法和静态静态):最终方法,实例方法不能被子类覆盖,静态方法不能被隐藏
- Object类的getClass( )
final类:最终类,不能派生子类。
- String, StringBuffer
- Math
三、抽象类和接口
抽象类和抽象方法的声明必须加上abstract关键字。
Java可定义不含方法体的方法,其方法体由子类根据具体情况实现,这样的方法称为抽象方法(abstract method),包含抽象方法的类必须是抽象类(abstract class)。
-
包含抽象方法的类必须是抽象类
-
抽象类和抽象方法必须用abstract关键字修饰
-
没有包含抽象方法的类也可以定义成抽象类
抽象方法:使用abstract定义的方法或者接口中定义的方法(接口中定义的方法自动是抽象的,可以省略abstract)。
抽象类不能被实例化。
接口里方法编译器自动加上public abstract来修饰。
只有实例方法可以声明为抽象方法。
抽象类不能被实例化,即不能用new关键字创建对象(即new 右边的类型不能是抽象类)。
但是抽象类可以作为变量声明类型、方法参数类型、方法返回类型
为什么?因为一个抽象类型引用变量可以指向具体子类的对象
接口是公共静态常量和公共抽象实例方法的集合。接口是能力、规范、协议的反映。
接口不是类:(1)不能定义构造函数;(2)接口之间可以多继承,类可implements多个接口。(3)和抽象类一样,不能new一个接口
接口中的所有数据字段隐含为public static final
接口体中的所有方法隐含为public abstract
接口不是类(Java支持单继承类),一个接口可以继承多个接口。
任何实现该接口的类,必须实现该接口继承的其他接口。
空接口称为标记接口(markup interface)
空接口有什么作用?唯一目的允许你用instanceof检查对象的类型:
if(obj instanceof Cloneable)…
包装类没有无参构造方法
每一个数值包装类都有相应类型常量MAX_VALUE和MIN_VALUE。
JDK1.5开始允许基本类型和包装类之间的自动转换。
- 将基本类型的值转换为包装类对象,称为装箱(boxing)
- 将包装类对象转换为基本类型的值,称为开箱(unboxing)
四、异常和IO
异常产生的原因
-
Java虚拟机同步检测到一个异常的执行条件,间接抛出异常,例如:
- 表达式违反了正常的语义,例如整数除零。
- 通过空引用访问实例变量或方法。
- 访问数组超界。
- 资源超出了某些限制,例如使用了过多的内存。
…
-
显式地执行throw语句抛出异常
-
Java异常都必须继承Throwable的直接或间接子类。用户通过继承自定义异常。
-
Java的异常分为二大类:从Exception派生的是程序级错误,可由程序本身处理;从Error派生是系统级错误,
程序可不用处理(也基本上处理不了,例如JVM内存空间不够)。
-
Exception的子类里,除了RuntimeException这个分支外,其他的都是必检异常(即:要么在函数里用catch子句捕获并处理,要么在所在函数加上异常声明,PPT第5页例子)。 RuntimeException的子类是非必检异常
非必检异常(Unchecked Exception)是运行时异常(RuntimeException)和错误(Error)类及它们的子类, 非必检异常在方法里可不捕获异常同时方法头可不声明异常,编译器不会报错。但该发生的异常还是要发生。
其它的异常称为必检异常(Checked Exception),编译器确保必检异常被捕获或声明(即要不在方法里捕获异常,要不在方法头声明异常)
捕获:方法可以通过try/catch语句来捕获异常
声明:方法可以在方法头使用throws子句声明可能抛出异常
方法可以抛出的异常
方法里调用throw语句直接抛出的任何异常
调用另一个方法时,由被调用方法间接抛出的异常
如果方法不捕获其中发生的必检异常,那么方法必须声明它可能抛出的这些异常
一个try块后面可以有多个catch块。每个catch块可以处理的异常类型由异常类型参数指定。异常参数类型必须是从Throwable派生的类。
当try块中的语句抛出异常对象时,运行时系统将调用第一个异常对象类型与参数类型匹配的catch子句。如果被抛出的异常对象可以被合法地赋值给catch子句的参数,那么系统就认为它是匹配的(和方法调用传参一样,子类异常对象匹配父类型异常参数类型)。
无论try块中是否发生异常,都会执行finally块中的代码。通常用于关闭文件或释放其它系统资源。
public static String read(String filePath){
String s = null;
BufferedReader reader = null; //BufferedReader一次读文本文件一行
try{
StringBuffer buf = new StringBuffer();
reader = new BufferedReader(new InputStreamReader(new FileInputStream(new
File(filePath))));
while( (s = reader.readLine()) != null){//readLine方法读取到文件末尾返回null
buf.append(s).append("\n");
}
s = buf.toString().trim();
}
catch (FileNotFoundException e) { e.printStackTrace();}
catch (IOException e) { e.printStackTrace()}
finally {
if(reader != null) {
try { reader.close()} //由于close也可能出错,也要异常处理
catch (IOException e) { e.printStackTrace();}
}
}
return s;
}
方法read内部已经处理了所有可能发生的异常,因此方法收不不需要加throws声明,同时read方法的调用代码不需要try/catch。
非必检异常(Unchecked Exception)是运行时异常(RuntimeException)和错误(Error)类及它们的子类, 方法可以不捕获同时不声明非必检异常(注意只是编译器不检查了,但如果真的有异常该抛出还是会抛出)
无论何时,throw以后的语句都不会执行。
无论同层catch子句是否捕获、处理本层的异常(即使在catch块里抛出或转发异常),同层的finally总是都会执行。
自定义异常类必须继承Throwable或其子类。
自定义异常类通常继承Exception及其子类,因为Exception是程序可处理的类。
如果自定义异常类在父类的基础上增加了成员变量,通常需要覆盖toString函数。
自定义异常类通常不必定义clone:捕获和处理异常时通常只是引用异常对象而已。
import java.lang.Exception;
public class ValueBeyondRangeException extends Exception{
int value, range;
public ValueBeyondRangeException(int v, int r){ value=v; range=r; }
public toString( ){
return value + ” beyonds “ + range;
}
}
//使用例子
int v = 1000,range = 100;
try{
if(v > range)
throw new ValueBeyondRangeException (v,range);
}
catch(ValueBeyondRangeException e){ System.out.println(e.toString( )); }