多态
多态定义
多态:可以理解为事物存在的多种体现形态。
例如:
人:男人,女人
动物:猫,狗
猫这个对象对应的类型是猫类型:猫 x = new 猫();
,同时猫也是动物中的一种,也可以把猫称为动物:动物 x = new 猫();
。动物是猫和狗等具体事物中抽取出来的父类型。
本文从以下几个方面介绍多态:
- 多态的体现——父类的引用指向了自己的子类对象。即父类的引用也可以接收自己的子类对象
- 多态的前提——必须是类与类之间有关系,要么继承,要么实现。通常还有一个前提:存在覆盖
- 多态的好处——多态的出现大大的提高了程序的扩展性
- 多态的弊端——提高了扩展性,但是只能使用父类的引用访问父类中的成员,不能预先使用子类,因为那时子类还没存在
- 多态的应用
- 多态的出现在代码中的特点(多态使用的注意事项)
以动物:猫,狗,猪为例说之。
abstract class Animal {
public abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
public void catchMouse() {
System.out.println("抓老鼠");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
public void kanJia() {
System.out.println("看家");
}
}
class Pig extends Animal {
public void eat() {
System.out.println("饲料");
}
public void gongDi() {
System.out.println("拱地");
}
}
那么以下代码:
Animal a = new Cat(); // 类型提升,向上转型
a.eat();
如果想要调用猫的特有方法时,如何操作?——强制将父类的引用转成子类类型,向下转型,即:
Cat c = (Cat)a;
c.catchMouse();
千万不要出现这样的操作,就是将父类对象转成子类类型,即:
Animal a = new Animal();
Cat c = (Cat)a;
我们能转换的是父类引用指向了自己的子类对象时,该引用可以被提升,也可以被强制转换。多态自始至终都是子类对象在做着变化。
instanceof关键字的使用
当我们使用向下转型时,确实是可以带来好处,即可以使用子类的特有功能。但是也会带来弊端——必须得面对具体的子类型。这即是说向下转型有风险,容易发生ClassCastException,只要转换类型和对象类型不匹配就会发生,想要安全,必须要进行判断,判断一个对象是否匹配某一个类型,这时我们就需要使用一个新的关键字——instanceof了,instanceof的用法为:对象 instanceof 类型
。我们举例说明:
class DuoTaiDemo2 {
public static void main(String[] args) {
function(new Dog());
function(new Cat());
}
public static void function(Animal a) {
a.eat();
if(a instanceof Cat) {
Cat c = (Cat)a;
c.catchMouse();
} else if(a instanceof Dog) {
Dog d = (Dog)a;
d.kanJia();
}
}
}
多态特点
在多态中,成员变量的特点:当子父类中出现同名变量时,多态调用时,只看调用该成员变量的引用所属的类中的成员变量。简单说:无论编译还是运行,都看等号(=)的左边(引用型变量所属的类)。
明白了在多态中成员变量的特点之后,试着看以下代码的运行结果:class Fu { int num = 5; void show() { System.out.println("num = " + this.num); // 打印num = 5 } } class Zi extends Fu { int num = 6; } class DuoTaiTest2 { public static void main(String[] args) { Fu f = new Zi(); f.show(); } }
很显然打印的是
num = 5
,为什么会这样呢?一图以蔽之:
- 在多态中成员函数(非静态)的特点:出现一模一样函数时,多态调用,在编译时期,参阅引用型变量所属的类中是否有调用的方法,如果有,编译通过,如果没有编译失败。在运行时期,参阅对象所属的类中是否有调用的方法。简单总结就是:成员函数(非静态)在多态调用时,编译看左边,运行看右边。
在一些专业书上也有这样的表述:成员方法动态绑定到当前对象上。 - 在多态中,静态成员函数(或者静态成员变量)的特点:出现一模一样函数时,多态调用,无论编译和运行,都参考左边(引用型变量所属的类)。其实我们要知道,真正调用静态方法是不需要对象的,直接类名调用。因为静态方法绑定到类上,所以这种情况更多用于面试中。
多态应用
例1,基础班学生:学习,睡觉;高级班学生:学习,睡觉。可以将这两类事物进行抽取。
abstract class Student {
public abstract void study();
public void sleep() {
System.out.println("躺着睡");
}
}
// 工具类
class DoStudent {
public void doSomething(Student stu) {
stu.study();
stu.sleep();
}
}
class BaseStudent extends Student {
public void study() {
System.out.println("base study");
}
public void sleep() {
System.out.println("坐着睡");
}
}
class AdvStudent extends Student {
public void study() {
System.out.println("adv study");
}
}
class DuoTaiDemo {
public static void main(String[] args) {
DoStudent ds = new DoStudent();
ds.doSomething(new BaseStudent());
ds.doSomething(new AdvStudent());
}
}
例2,需求:电脑运行示例,电脑运行基于主板。
// 接口定义规则
interface PCI {
public void open();
public void close();
}
class MainBoard {
public void run() {
System.out.println("mainboard run");
}
public void usePCI(PCI p) { // PCI p = new NetCard(); // 接口型引用指向自己的子类对象
if(p != null) {
p.open();
p.close();
}
}
}
class NetCard implements PCI {
public void open() {
System.out.println("netcard open");
}
public void close() {
System.out.println("netcard close");
}
}
class SoundCard implements PCI {
public void open() {
System.out.println("soundcard open");
}
public void close() {
System.out.println("soundcard close");
}
}
class DuoTaiDemo {
public static void main(String[] args) {
MainBoard mb = new MainBoard();
mb.run();
mb.usePCI(null);
mb.usePCI(new NetCard());
mb.usePCI(new SoundCard());
}
}
示意图:
例3,数据库的操作。数据是:用户信息。
- 连接数据库(JDBC Hibernate)
- 操作数据库(CRUD)——
C creat
R read
U update
D delete
- 关闭数据库连接
interface UserInfoDao {
public void add(User user);
public void delete(User user);
}
class UserInfoByJDBC implements UserInfoDao {
public void add(User user) {
1、JDBC连接数据库
2、使用sql添加语句添加数据
3、关闭连接
}
public void delete(User user) {
1、JDBC连接数据库
2、使用sql删除语句删除数据
3、关闭连接
}
}
class UserInfoByHibernate implements UserInfoDao {
public void add(User user) {
1、Hibernate连接数据库
2、使用sql添加语句添加数据
3、关闭连接
}
public void delete(User user) {
1、Hibernate连接数据库
2、使用sql删除语句删除数据
3、关闭连接
}
}
class DBOperate {
public static void main(String[] args) {
UserInfoDao ui = new UserInfoByHibernate();
ui.add(user);
ui.delete(user);
}
}
示意图如下:
Object类
Object是所有对象的直接或者间接父类,传说中的上帝。该类中定义的肯定是所有对象都具备的功能。
Object类中已经提供了对对象是否相同的比较方法,如果自定义类中也有比较相同的功能,没有必要重新定义。只要沿袭父类中的功能,建立自己特有比较内容即可,这就是覆盖。
例,假如有一个Person类,其代码为:
class Person extends Object
{
private int age;
private String name;
Person(String name, int age)
{
this.name = name;
this.age = age;
}
}
现在我们的需求是:定义一个方法,判断两个Person对象是否是同一个,判断的依据是根据姓名和年龄,如果姓名和年龄都相同,视为同一个人。
我的分析:不用再自定义方法判断对象是否相同了,因为在Object父类中,已经定义了这样的方法,直接使用就可以了,但是判断的内容是根据Person的特点定义的,那就需要保留父类的功能声明,定义子类功能的特有内容,使用覆盖。所以要在Person类中加入如下equals方法:
public boolean equals(Object obj) // Object obj = p2;--->Object obj = new Person();
{
// 提高点效率。如果两个引用指向了同一个对象,就不用再转换并比较内容了,直接判断地址就哦了。
if (this == obj)
return true;
// obj.age是错误的,因为Object中没有age属性,
// 想要使用子类对象的特有属性或行为,必须对其进行向下转型,并且需要进行类型判断
if (!(obj instanceof Person))
{
// return false;
throw new ClassCastException("类型错误");
}
Person p = (Person)obj;
// 如果判断姓名字符串是否相同,不要用==,字符串本身是一个对象,所以要使用String类的equals方法
return this.name.equals(p.name) && this.age == p.age;
}
有时候,我们还需要重写Object类的toString()方法,建立Person对象特有的字符串表现形式。查询API帮助文档,我们可以发现:Object
类的toString
方法返回一个字符串,该字符串由类名(对象是该类的一个实例)、at标记符“@”和此对象哈希码的无符号十六进制表示组成。换句话说,该方法返回一个字符串,它的值等于:getClass().getName() + '@' + Integer.toHexString(hashCode())
。如果我们不在Person类中重写该方法,运行以下程序:
class ObjectDemo
{
public static void main(String[] args)
{
Person p1 = new Person("lisi", 21);
Person p2 = new Person("mazi", 25);
System.out.println(p1); // Person@139a55,打印对象时,默认调用toString()方法
System.out.println(p1.toString());
}
}
则会输出Person@139a55
,而且在打印对象时,默认调用了toString()方法。
顺便说一下,如果要我们自己来弄,输出
Person@139a55
这样的东西,又该怎么做呢?
这时,我们就要接触一点点反射的知识了,关于反射的知识以后会另开一篇专讲。A.class
,B.class
这些class
文件都有名称,这些文件内都有构造函数,一般方法,java中用Class
来描述这些class
文件,可通过getName()
获取名称。所以以下代码:Person p1 = new Person("lisi", 21); Class c = p1.getClass(); System.out.println(c.getName()); // Person
会输出
Person
。
现在我们就可以自己来编写了,代码如下:Person p1 = new Person("lisi", 21); Class c = p1.getClass(); System.out.println(c.getName()+"@@"+Integer.toHexString(p1.hashCode()));
这时就会输出
Person@@139a55
这样的东西了。
说完toString()方法,我们须在Person类中重写该方法,所以应在Person类中添加如下toString()方法:
/**
建立Person对象特有的字符串表现形式,只要覆盖toString方法即可
*/
public String toString()
{
return "Person[name = " + this.name +", age = " + this.age + "]";
}
这样,测试类的代码可这样写为:
class ObjectDemo
{
public static void main(String[] args)
{
Person p1 = new Person("lisi", 21);
Person p2 = new Person("mazi", 25);
System.out.println(p1.equals(p2)); // 判断的是对象的内容,用equals。
System.out.println(p1 == p2); // 判断的是对象的地址
}
}
内部类
将一个类定义在另一个类的里面,对里面那个类就称为内部类(内置类,嵌套类)。例如,A类要直接访问B类中的成员时,可以将A类直接定义到B类中,作为B类的内部类存在。
内部类的访问规则:
内部类可以直接访问外部类中的成员,包括私有。之所以可以直接访问外部类中的成员,是因为内部类中持有了一个外部类中的引用,格式:
外部类名.this
。用一个例子来验证:class Outer { int num = 2; class Inner { int num = 3; void show() { int num = 4; System.out.println("show..." + Outer.this.num); } } public void method() { new Inner().show(); } } class InnerClassDemo2 { public static void main(String[] args) { new Outer().method(); } }
外部类要想访问内部类,只能创建内部类的对象来访问。
访问格式:
非静态,非私有的内部类访问方式
当内部类定义在外部类的成员位置上,而且非私有,可以在外部其他类中,可以直接建立内部类对象。格式为:外部类名.内部类名 变量名 = 外部类对象.内部类对象;
,例,Outer.Inner in = new Outer().new Inner();
示例代码如下:class Outer { private int num = 4; class Inner // 内部类,相当于外部类中的一个成员,它就可以被成员修饰符所修饰,public private static { void show() { System.out.println(num); } } } class InnerClassDemo { public static void main(String[] args) { Outer.Inner in = new Outer().new Inner(); in.show(); } }
而且在非静态内部类中只允许定义静态的常量,存于常量池中,不能定义其他静态成员。所以,以下代码编译是没有任何问题的:
class Outer { private int num = 4; class Inner // 内部类,相当于外部类中的一个成员,它就可以被成员修饰符所修饰,public private static { static final int count = 5; // 在非静态内部类中只允许定义静态的常量,存于常量池中,不能定义其他静态成员 void show() { System.out.println(num); } } } class InnerClassDemo { public static void main(String[] args) { Outer.Inner in = new Outer().new Inner(); in.show(); } }
静态,非私有的内部类访问方式,访问非静态成员
当内部类在成员位置上时,就可以被成员修饰符修饰,比如,private
将内部类在外部类中进行封装,static内部类
就具备static
的特性。当内部类被static
修饰后,只能直接访问外部类中的static
成员,出现了访问局限。
在外部其他类中,如何直接访问静态内部类非静态成员呢?答案是格式为new Outer.Inner().function();
。示例代码如下:class Outer { private static int num = 4; class Inner // 内部类,相当于外部类中的一个成员,它就可以被成员修饰符所修饰,public private static { void show() { System.out.println(num); } } static class Inner2 // 静态内部类,相当于一个外部类 { void show2() { System.out.println("show2..." + num); } } } class InnerClassDemo { public static void main(String[] args) { Outer.Inner2 in = new Outer.Inner2(); in.show2(); } }
静态,非私有的内部类访问方式,访问静态成员
在外部其他类中,如何直接访问静态内部类静态成员呢?答案是格式为Outer.Inner.function();
。示例代码如下:class Outer { private static int num = 4; class Inner // 内部类,相当于外部类中的一个成员,它就可以被成员修饰符所修饰,public private static { void show() { System.out.println(num); } } static class Inner2 // 静态内部类,相当于一个外部类 { void show2() { System.out.println("show2..." + num); } static void show3() { System.out.println("show3..." + num); } } } class InnerClassDemo { public static void main(String[] args) { Outer.Inner2.show3(); } }
注意:当内部类中定义了静态成员,该内部类必须是static的。当外部类中的静态方法访问内部类时,内部类也必须是static的。
class Outer {
private static int x = 3;
static class Inner { // 静态内部类
static void function() { // 当内部类中定义了静态成员,该内部类必须是static的
System.out.println("inner::::"+x); // 当内部类被static修饰后,只能直接访问外部类中的static成员
}
}
static class Inner2 {
void show() {
System.out.println("inner2 show");
}
}
public static void method() {
new Inner2().show(); // 当外部类中的静态方法访问内部类时,内部类也必须是static的
}
}
内部类定义在局部时
- 不可以被成员修饰符修饰,因为
private
、static
不能修饰局部成员。 可以直接访问外部类中的成员,因为还持有外部类中的引用。但是不可以访问它所在的局部中的变量,只能访问被
final
修饰的局部变量,主要原因是生命周期不同。注意:java8没这个区别了,但是被final修饰的变量是一个常量,只能被赋值一次,所以一经存在就不得更改。
例,以下是java8的运行环境。class Outer { int x = 3; void method(int a) { // a++; // 从内部类引用的本地变量必须是最终变量或实际上的最终变量 int y = 4; class Inner { void function() { System.out.println(a); } } new Inner().function(); } } class InnerClassDemo { public static void main(String[] args) { Outer out = new Outer(); out.method(7); out.method(8); } }
为了能说明局部内部类只能访问被final修饰的局部变量,而且其主要原因是生命周期不同这一点,我们举例验证。
class Outer
{
private int num = 4;
Object obj;
public void method()
{
/*final*/ int x = 5;
class Inner extends Object // Inner本身继承Object
{
// 覆盖Object类中的toString()方法
public String toString()
{
System.out.println("x = " + 5);
System.out.println("show..." + num);
return "Inner...abc";
}
}
// 创建内部类的对象
Inner in = new Inner();
// 将内部类对象的地址赋值给obj
obj = in;
}
public void function()
{
// 打印obj指向的对象的字符串表现形式
System.out.println(obj.toString());
}
}
class InnerClassDemo2
{
public static void main(String[] args)
{
new Outer().method();
}
}
以上例子在JVM内存中的示意图大概是这样的:
匿名内部类
- 匿名内部类其实就是内部类的简写格式。
- 定义匿名内部类的前提:内部类必须是继承一个类或者实现接口。
- 匿名内部类的格式:
new 父类或者接口() {定义子类的内容}
。 - 匿名内部类其实就是一个匿名子类对象,而且这个对象有点胖。可以理解为带内容的对象。
匿名内部类中定义的方法最后不要超过3个。
例,abstract class AbsDemo { abstract void show(); } class Outer { int x = 3; public void function() { AbsDemo d = new AbsDemo() { int num = 9; void show() { System.out.println("num==="+num); } void abc() { System.out.println("haha"); } }; d.show(); // d.abc(); // 编译失败,因为父类中没有这个方法 } }
练习一:补全代码,通过匿名内部类。
interface Inter {
void method();
}
class Test {
// 补足代码。通过匿名内部类
}
class InnerClassTest {
public static void main(String[] args) {
Test.function().method();
}
}
解:
interface Inter {
void method();
}
class Test {
// 补足代码。通过匿名内部类
static Inter function() {
return new Inter() {
public void method() {
System.out.println("Inter method");
}
};
}
}
class InnerClassTest {
public static void main(String[] args) {
// Test.function():Test类中有一个静态的方法function
// .method():function这个方法运算后的结果是一个对象,而且是一个Inter类型的对象,
// 因为只有是Inter类型的对象,才可以调用method()
Test.function().method();
}
}
面试时可能遇到的一个小问题(有关匿名内部类的),如果没有一个类继承或一个接口实现,还能使用匿名内部类吗?答案是可以的。
class InnerTest {
public static void main(String[] args) {
new Object() { // new Object() {}是Object类的子类对象
public void function() {
System.out.println("hello");
}
}.function();
}
}