Java作为一门完全面向对象的语言,笔者感到其威力体现在面向对象的概念大大抽象了程序繁琐的细节,提取了程序间的共性和联系,使得开发者可以投入更多精力在设计程序而不是写代码上。由于Java面向对象的内容过多,这里只列举一些容易混淆和出错的地方。毕竟写技术日记的目的是为了利于学习,胡子眉毛一把抓就无法提高效率。
1. 多态
1.1 重载的条件
方法的重载必须满足以下全部条件:
方法名相同;
参数列表不同;
返回类型可以相同也可以不同;
访问权限可以相同也可以不同。
其中参数列表不同可以是参数的类型不同,个数不同或者顺序不同。因为方法名和参数列表共同构成了函数签名(signature),执行某个方法时JVM看到的只是函数签名。
正确的重载:
public class Test
{
public static void main(String[] args)
{
Add a = new Add();
System.out.println(a.add(1, 1));
System.out.println(a.add(1, 1, 1));
}
}
class Add
{
public int add(int x, int y)
{
return x + y;
}
public int add(int x, int y, int z)
{
return x + y + z;
}
}
同样函数签名的方法是不允许同时出现在一个类中的,否则JVM无法决定执行哪个方法,比如:
错误的重载:
class Add
{
public int add(int x, int y)
{
return x + y;
}
public char add(int x, int y) //报错,已在Add中定义add(int, int)
{
return (char)(x + y);
}
}
正确的重载:
class Add
{
public int add(int x, int y)
{
return x + y;
}
public char add(int x, int y, int z)
{
return (char)(x + y + z);
}
}
两个add方法参数列表不同,返回值类型也可以不同,扔属于重载。
1.2 重写的条件
方法的重写必须满足以下全部条件:
子类重写方法的名称,参数签名和返回类型必须与父类原方法的名称,参数签名和返回类型一致;
重写方法的访问权限大于等于原方法;
重写方法的异常抛出范围小于等于原方法;
重写只能发生在子类和父类之间,重写方法和原方法必须分别存在于子类和父类中;
重写只是针对方法,属性不能被重写,属性跟引用变量的声明类型绑定,
静态方法不能被重写,因为静态方法跟其被声明的类绑定。
正确的重写:
子类重写方法的名称,参数签名和返回类型必须与父类原方法的名称,参数签名和返回类型一致;
重写方法的访问权限大于等于原方法;
重写方法的异常抛出范围小于等于原方法;
重写只能发生在子类和父类之间,重写方法和原方法必须分别存在于子类和父类中;
重写只是针对方法,属性不能被重写,属性跟引用变量的声明类型绑定,
静态方法不能被重写,因为静态方法跟其被声明的类绑定。
正确的重写:
public class Test
{
public static void main(String[] args)
{
Child c = new Child();
c.display(); //输出:Child: Child, 调用的是子类的重写方法
}
}
class Parent
{
void display()
{
System.out.println(this.getClass().getName() + ": " + "Parent");
}
}
class Child extends Parent
{
void display()
{
System.out.println(this.getClass().getName() + ": " + "Child");
}
}
1.3 动态绑定
动态绑定是指一个引用变量调用一个方法时,如果这个方法重写了其他方法或者被其他方法重写,则该方法的具体执行内容需要在程序运行时才确定,而且取决于该引用指向的堆内存中的对象类型而非该引用的声明类型。比如:
public class Test
{
public static void main(String[] args)
{
//PP情况
Parent pp = new Parent(); //输出:Parent: Parent
pp.display();
//PC情况
Parent pc = new Child(); //输出:Child: Child
pc.display();
//CP情况
//Child cp = new Parent(); //报错:不兼容的类型
//cp.display();
//CC情况
Child cc = new Child(); //输出:Child: Child
cc.display();
}
}
class Parent
{
void display()
{
System.out.println(this.getClass().getName() + ": " + "Parent");
}
}
class Child extends Parent
{
void display()
{
System.out.println(this.getClass().getName() + ": " + "Child");
}
}
由上面例子可见,PP和CC情况下引用变量的声明类型和指向的对象类型一致,则调用其声明类中的方法。CP情况报错,因为属于子类的声明类型的引用若要被赋值为属于父类的对象类型,需要上转换,而此处无法进行上转换。比较复杂的是PC情况,这种情况体现了动态绑定的思想,我们来分析一下:
Parent pc = new Child();
pc.display();
首先,编译器根据引用的声明类型(Parent)和方法名(display),搜索相应类(Parent)及其子类(Child)的“方法表”,找出所有名字为display的方法。可能存在多个方法名为display的方法,只是参数类型或数量不同。然后,根据函数签名找出完全匹配的方法。因为display方法被重写,所以父子类中的display方法都符合要求。如果display方法的访问权限为private,或者访问修饰符为static或者final,则立刻可以明确这两个display方法选哪一个执行(事实上有可能不会出现两个重名方法),因为private方法不能被继承也就不能被重写,不会出现重名的方法,static方法与类绑定按调用该方法的引用的声明类型执行,final方法与private方法类似,无法继承和重写。经过这三步编译阶段结束。如果在这三步中能确定父子类中重名的display方法(一个是父类的,一个是被子类重写的)哪一个应当被引用变量调用,则称其为静态绑定。
如果不能确定,则需要在运行阶段进行第四步:根据引用变量指向的对象类型来确定调用哪个方法。这里pc引用变量指向的对象类型是Child类型,所以调用Child类型中重写后的display方法。
2. 对象
2.1 生命周期
对象有三种生命周期:
离开作用域:
离开作用域:
{
Person p1 = new Person(); //离开作用域时,p1失效,Person对象成为垃圾
}
引用变量指向null:
{
Person p1 = new Person();
p1 = null; //p1失效,Person对象成为垃圾
}
引用变量的赋值可以延长生命周期:
{
Person p1 = new Person();
Person p2 = p1; //p1, p2两个引用变量指向同一个Person对象
p1 = null; //p1失效,但p2仍指向Person对象,直到超出作用域后成为垃圾
}
2.2 比较
有2种方式可用与对象间的比较,== 与equals()方法。==操作符用于比较两个变量的值(内存地址)是否相等,比如基本数据类型间的比较。equals()方法用于比较两个对象的内容是否一致:
public class Test
{
public static void main(String[] args)
{
String str1 = new String("abc");
String str2 = new String("abc");
String str3 = str1;
System.out.println(str1 == str2); //false, 两个引用分别指向堆内存中的两个对象
System.out.println(str1 == str3); //true, 两个引用的值相等
}
}
2.3 构造方法
一个类每创建一个对象,构造方法都会被调用一次。构造方法是public的,亦可为private(如单态设计模式),与类名相同。构造方法没有返回值,没有return语句,可以重载。
特别需要注意的是,如果一个类中输入了有参构造方法,则其默认生成的无参构造方法不再生成。如果父类使用了有参构造方法,则子类在生成对象时必须用super关键字调用父类的有参构造方法,否则子类对象无法生成,比如:
特别需要注意的是,如果一个类中输入了有参构造方法,则其默认生成的无参构造方法不再生成。如果父类使用了有参构造方法,则子类在生成对象时必须用super关键字调用父类的有参构造方法,否则子类对象无法生成,比如:
public class Test
{
public static void main(String[] args)
{
Parent p = new Parent(1);
System.out.println(p); //输出1
Child c = new Child(2);
System.out.println(c); //输出3, 2
}
}
class Parent
{
int x;
Parent(int x)
//输入了有参构造方法,默认的无参构造方法不再生成,子类也不可能继承
{
this.x = x;
}
@Override
public String toString()
{
return String.valueOf(x);
}
}
class Child extends Parent
{
int y;
Child(int y) //子类构造方法中调用父类构造方法
{
super(3); //super关键字调用父类的有参构造方法,给x赋值
this.y = y; //子类构造方法给子类独有的y属性赋值
}
@Override
public String toString()
{
return String.valueOf(x) + ", " + String.valueOf(y);
}
}
2.4 this关键字
this关键字是个引用变量,this引用指向调用其所在的方法的对象,也就是当前对象。this主要用在:
区分重名的本类属性和形参变量:
区分重名的本类属性和形参变量:
public class Test
{
public static void main(String[] args)
{
Student stu = new Student("Jack");
}
}
class Student
{
String name;
Student(String name)
{
this.name = name; //this.name代表本类属性
}
}
通过this引用把当前的对象作为一个参数传递给其他的方法,通常用于生成一个包含其它对象引用的对象:
public class Test
{
public static void main(String[] args)
{
School sch = new School();
sch.addStudent();
}
}
class School
{
Student stu;
public void addStudent()
{
stu = new Student("Jack", this); //this将调用本addStudent方法的School对象的引用传递给新的Student对象
}
}
class Student
{
String name;
School sch;
Student(String name, School sch) //sch引用即是School类中的this
{
this.name = name;
this.sch = sch;
}
}
构造方法是在产生对象时被Java系统自动调用的,我们不能在程序中像其他调用其他方法一样去调用构造方法。但是我们可以通过this在一个构造方法里调用执行其他重载的构造方法:
class Student
{
String name;
School sch;
Student(String name)
{
this.name = name;
}
Student(String name, School sch)
{
this(name); //this调用了前面的重载构造方法给name属性赋了值,本构造方法给sch赋值
this.sch = sch;
}
}
3. 参数传递
3.1 基本类型变量的参数传递
方法的形式参数就相当于方法中的局部变量,方法调用结束时被释放,并不会影响到主程序中同名的局部变量。 基本类型的变量作为实参传递,并不能改变这个变量的值。例如:public class Test
{
public static void main(String[] args)
{
int x = 1; //x存在于main静态方法的栈区中
change(x);
System.out.println(x);
}
public static void change(int x)
{
x = 2; //x存在于change静态方法的栈区中,不会改变main方法中的x
}
}
String类型的传递规则与基本类型的传递规则一致。
3.2 引用类型变量的参数传递
Java语言在被给调用方法的参数赋值时,只采用传值的方式。所以,基本数据类型传递的是该数据的值本身,而引用数据类型传递的也是这个变量的值本身,即对象的引用(句柄),而非对象本身,但通过方法调用改变了对象的内容,但是对象的引用是不能改变的,所以最终值发生了变量。数组也属于引用数据类型。
public class Test
{
public static void main(String[] args)
{
int[] arr = {1, 2}; //引用arr存在于main静态方法栈区
change(arr);
for(int tmp : arr)
{
System.out.print(tmp + " "); //输出1, 1
}
}
public static void change(int[] arr)
{
arr[1] = 1; //同一个引用arr
}
}
4. 内部类
4.1 定义
在一个类内部定义类,这就是嵌套类(nested classes),也叫内部类、内置类。
内部类可以直接访问嵌套它的类的成员,包括private成员,但是嵌套类的成员却不能被嵌套它的类直接访问,比如:
内部类可以直接访问嵌套它的类的成员,包括private成员,但是嵌套类的成员却不能被嵌套它的类直接访问,比如:
public class Test
{
public static void main(String[] args)
{
Outer.Inner in = new Outer().new Inner(); //先生成外部类的实例,再生成内部类的实例
in.display(); //输出outer
}
}
class Outer
{
String outer_str = "outer";
void display()
{
//System.out.println(Inner.inner_str); //报错:无法在静态上下文中引用非静态变量
}
class Inner
{
String inner_str = "inner";
void display()
{
System.out.println(outer_str);
}
}
}
当一个类中的程序代码要用到另外一个类的实例对象,而另外一个类中的程序代码又要访问第一个类中的成员,就应当将另外一个类做成第一个类的内部类。
4.2 外部访问内部类
虽然外部类的方法不能直接访问内部类,但可以通过调用内部类对象的方法在外部类之外访问内部类,只要欲访问的内部类属性、方法权限比private高即可,比如:
public class Test
{
public static void main(String[] args)
{
Outer.Inner in = new Outer().new Inner();
System.out.println(in.inner_str); //输出inner
}
}
class Outer
{
String outer_str = "outer";
class Inner
{
String inner_str = "inner";
void display()
{
System.out.println(outer_str);
}
}
}
4.3 匿名内部类
匿名类是不能有名称的类,所以没办法引用它们。必须在创建时,作为new语句的一部分来声明它们。这就要采用另一种形式的new语句,如下所示: new <类或接口> <类的主体> 这种形式的new语句声明一个新的匿名类,它对一个给定的类进行扩展,或者实现一个给定的接口。它还创建那个类的一个新实例,并把它作为语句的结果而返回。要扩展的类和要实现的接口是new语句的操作数,后跟匿名类的主体。如果匿名类对另一个类进行扩展,它的主体可以访问类的成员、覆盖它的方法等等,这和其他任何标准的类都是一样的。如果匿名类实现了一个接口,它的主体必须实现接口的方法。比如经常用到匿名内部类的事件监听程序:
import java.awt.*;
import java.awt.event.*;
public class QFrame extends Frame
{
public QFrame()
{
this.setTitle("my application");
addWindowListener
(
new WindowAdapter() //WindowAdapter是个抽象类,匿名内部类继承了这个抽象类
//匿名内部类开始
{ //可以通过new .....()后面紧跟的{}来判断这是匿名内部类的开始
//重写WindowAdapter的windowClosing方法
public void windowClosing(WindowEvent e)
{
dispose();
System.exit(0);
}
}
//匿名内部类结束
); //有分号
this.setBounds(10,10,200,200);
}
}
在使用匿名内部类时,有一些需要特别注意的限制:
匿名内部类不能有构造方法。
匿名内部类不能定义任何静态成员、方法和类。
匿名内部类不能是public,protected,private,static。
只能创建匿名内部类的一个实例。
一个匿名内部类一定是在new的后面,用其隐含实现一个接口或实现一个类。
因匿名内部类为内部类,所以内部类的所有限制都对其生效。
内部类只能访问外部类的静态变量或静态方法。
从上面的分析可以看出,匿名内部类就是继承了某个抽象类或者接口的内部类的简化写法。
5. 总结
面向对象作为Java语言的核心概念,应当重视。若要深入理解,应该多写一些试验程序,并思考相应的内存分配原理,才能从硬件行为的层面理解软件的规则。