第3节 面向对象高级
一、继承
1.1 概述和使用
继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
格式如下所述:
class 父类 {
}class 子类 extends 父类 {
}
继承的限制:Java中只有单继承,多重继承,没有多继承。
看下面的例子:
package com.kaikeba.objectoriented.senior;public class inherit2 {public static void main(String[] args) {Student2 s = new Student2();s.setName("张三");s.setAge(18);s.say();}
}class Person2 {private String name;private int age;public String sex;public Person2() {}public Person2(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public void say() {System.out.println("我是:"+name+",我今年"+age+"岁了");}
}class Student2 extends Person2 {
}
结果如下:
我是:张三,我今年18岁了
子类继承父类后,可以使用父类的属性和方法(public和protected修饰的)。
1.2 子类实例化内存分析
Student s = new Student()
内部的创建过程:
1、先查看Student
是否有父,若有,先在堆内存中创建一个父对象的空间,再创建一个Student
对象空间,同时super
指向父类对象,可以找到父对象。
2、s.setName("张三");
若发现Student
类中没有setName
方法,就会使用Person
的setName
,并将name
赋值到Person
对象中,若有就用自己的,这个后面说。
补充: 父类中只有用public
和protected
修饰符修饰的属性和方法才可以被子类使用,比如Student
对象无法使用Person
私有的属性,但是可以通过set
和get
来操作属性。
1.3 super关键字
this
表示当前对象:
- this.属性
区别成员变量和局部变量
- this.()
调用本类的某个方法
- this()
表示调用本类构造方法,只能用在构造方法的第一行语句。
- this
关键字只能出现在非static修饰的代码中
super
表示父类对象:
- super.属性
表示父类对象中的成员变量
- super.方法()
表示父类对象中定义的方法
- super()
表示调用父类构造方法
可以指定参数,比如super("Tom",23);
任何一个构造方法的第一行默认是super();
可以写上,如果未写,会隐式调用super();
- super()
只能在构造方法的第一行使用。
- this()
和super()
都只能在构造的第一行出现,所以只能选择其一。写了this()
就不会隐式调用super()
。
super
调用父类构造方法,必须满足父类中有对应的构造方法,见下述代码:
package com.kaikeba.objectoriented.senior;public class inherit2 {public static void main(String[] args) {Student2 s = new Student2();s.say();}
}class Person2 {private String name;private int age;public String sex;public Person2() {}public Person2(String name, int age) {this.name = name;this.age = age;}public void say() {System.out.println("我是:"+name+",我今年"+age+"岁了,性别:"+sex);}
}class Student2 extends Person2 {public Student2() {//super调用了父类的全参构造方法super("无名称", 1);super.sex = "男";}
}
结果如下:
我是:无名称,我今年1岁了,性别:男
1.4 重写(Override)与重载(Overload)
子类中可能需要对父类的一些方法进行修改,这就是重写的概念:
重写(override)规则:
-
1.参数列表必须完全与被重写方法的相同;
-
2.返回类型必须完全与被重写方法的返回类型相同;
-
3.访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为
public
,那么在子类中重写该方法就不能声明为protected
; -
4.父类的成员方法只能被它的子类重写。
-
5.声明为
static
和private
的方法不能被重写,但是能够被再次声明。
package com.kaikeba.objectoriented.senior;public class override {public static void main(String[] args) {Student3 s = new Student3();s.say();}
}class Person3 {//父类方法public void say() {System.out.println("锄禾日当午");}
}class Student3 extends Person3 {//子类对父类方法的重写public void say() {System.out.println("参数列表 与 返回值类型 必须相同");}// void say() {
// System.out.println("权限修饰符级别不能更低");
// }
}
面试中可能会考察重载(Overload)与重写(Override)的区别:
1. 发生的位置:
1. 重载:一个类中
2. 重写:子类对父类方法进行重写
2. 参数列表规则:
1. 重载:必须不同(长度,类型,顺序不同均可)
2. 重写:必须相同
3. 返回值类型:
1. 重载:与返回值类型无关
2. 重写:返回值类型必须一致
4. 访问权限:
1. 重载:与访问权限无关
2. 重写:子类的方法权限必须高于等于父类的方法权限
5. 异常处理:
1. 重载:与异常无关
2. 重写:异常范围可以更小,但是不能抛出新的异常
二、final关键字
final
关键字用于修饰属性、变量:
变量成为常量,无法对其再次进行赋值;
final
修饰的局部变量只能赋值一次(可以先声明后赋值);
final
修饰的成员属性,必须在声明时赋值。
public class finalkeyword {final int a = 10; //成员属性public static void main(String[] args) {final int a = 10; //局部变量}
}
全局常量(public static final
)
常量的命名规则:
由一个或多个单词组成,单词与单词之间必须使用下划线隔开,单词中所有字母大写,例如:SQL_INSERT
。
final
用于修饰类:
final
修饰的类,不可以被继承。
final
用于修饰方法:
final
修饰的方法,不可以被子类重写。
三、抽象类
3.1 概念
抽象类必须使用abstract class
声明;
格式:
abstract class 类名{ //抽象类
}
3.2 抽象方法
只声明而未实现的方法称为抽象方法(未实现指的是:没有"{}"方法体 ),抽象方法必须使用abstract关键字声明。
格式:
abstract class 类名 { //抽象类public abstract void 方法名(); //抽象方法,只声明而未实现
}
一个抽象类中可以没有抽象方法,抽象方法必须写在抽象类或者接口中。
3.3 几个原则
-
抽象类本身是不能直接进行实例化操作的,即:不能直接使用关键字
new
完成; -
一个抽象类必须被子类所继承,被继承的子类(如果不是抽象类)则必须覆写抽象类中的全部抽象方法。
3.4 常见问题
- 抽象类能否使用
final
声明?
不能,因为final
修饰的类是不能被子类继承的,而抽象类必须有子类才有意义,所以不能。
- 抽象类是否有构造方法?
能有构造方法,而且子类对象实例化的时候的流程与普通类的继承是一样的,都是要先调用父类中的构造方法(默认是无参的),之后再调用子类自己的构造方法。
3.5 抽象类与普通类的区别
-
抽象类必须用
public
或protected
修饰(如果为private
修饰,那么子类无法继承,也就无法实现其抽象方法),默认缺省为public
。 -
抽象类不可以使用
new
关键字创建对象,但是在子类创建对象时,抽象父类也会被JVM实例化。 -
如果一个子类继承抽象类,那么必须实现其所有的抽象方法。如果有未实现的抽象方法,那么子类也必须定义为
abstract
类。
四、接口
4.1 概念
如果一个类中全部方法都是抽象方法,全部属性都是全局常量,那么此时就可以将这个类定义成一个接口。
定义格式:
interface 接口名称{全局常量;抽象方法;
}
接口的抽象程度更高,后续项目中使用的也比抽象类更多一些。
4.2 面向接口编程思想
这种思想是定义(规范,约束)与实现(名实分离原则)的分离。从整体的角度先构思好接口,然后再去具体实现它。
优点:
-
降低程序的耦合性;
-
易于程序的扩展;
-
有利于程序的维护。
4.3 全局常量和抽象方法的简写
因为接口本身都是由全局常量和抽象方法组成的,所以接口中的成员定义可以简写:
- 全局常量编写时,可以省略
public static final
关键字,例如:
public static final String INFO = "内容" ;
简写后:
String INFO = "内容" ;
- 抽象方法编写时,可以省略
public abstract
关键字,例如:
public abstract void print() ;
简写后:
void print() ;
4.4 接口的 实现implements 与 继承extends
接口可以多实现:
格式:class 子类 implements 父接口1,父接口2...{}
以上的代码称为接口的实现。那么如果一个类即要实现接口,又要继承抽象类的话,则按照以下的格式编写即可:class 子类 extends 父类 implements 父接口1,父接口2...{}
接口因为都是抽象部分, 不存在具体的实现, 所以允许多继承,例如:interface C extends A,B{}
实现后的接口是一个类,继承后的接口还是一个接口。
注意: 如果一个接口想要使用,必须依靠子类。子类(如果不是抽象类的话)要实现接口中的所有抽象方法。
4.5 接口和抽象类的区别
-
抽象类要被子类继承,接口要被类实现;
-
接口只能声明抽象方法,抽象类中可以声明抽象方法,也可以写非抽象方法;
-
接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量;
-
抽象类使用继承来使用,无法多继承;接口使用实现来使用,可以多实现;
-
抽象类中可以包含static方法,但是接口中不允许(静态方法不能被子类重写,因此接口不能声明静态方法) ;
-
接口不能有构造方法,但是抽象类可以有。
五、多态
5.1 概念
多态就是对象的多种表现形式(多种体现形态)。
5.2 多态的体现
对象的多态性 :在类中有子类和父类之分,子类就是父类的一种形态,对象多态性就从此而来。
方法的重载和重写也是多态的一种,不过是方法的多态性 :
重载:一个类中方法的多态性体现
重写:子父类中方法的多态性体现
5.3 多态的使用:对象的类型转换
类似于基本数据类型的转换:
向上转型 :将子类实例变为父类实例
格式:父类 父类对象名 = 子类实例 ;
向下转型 :将父类实例变为子类实例
格式:子类 子类对象 = (子类) 父类实例 ;
package com.kaikeba.objectoriented.senior;public class polymorphicDemo {/*** 父类引用指向子类对象* @param args*/public static void main(String[] args) {
// Person5 p = null;
// Student5 s = new Student5();
// p = s; //s是p的一种形态
// p.say();Student5 a = new Student5();Nurse b = new Nurse();Person5 p1 = a;Person5 p2 = b;p1.say();p2.say();// Student5 a2 = (Student5) p1;Student5 a3 = (Student5) p2;a3.say();}
}
Student
实例可以向上转型为父类Person
实例,Person
实例也可以向下转型回Student
实例,但是不能将Nurse
实例转为Student
实例。
5.4 instanceof
作用:
判断某个对象是否是指定类的实例,则可以使用instanceof
关键字。
格式:
实例化对象 instanceof 类 //此操作返回boolean类型的数据
package com.kaikeba.objectoriented.senior;public class instanceofDemo {public static void main(String[] args) {Nurse6 n = new Nurse6();say(n);Student6 s = new Student6();say(s);}public static void say(Person6 p) {//如何判断传入的对象是此类型的哪种形态(哪个子类的对象)if(p instanceof Student6) {Student6 s = (Student6) p;s.say();}else {System.out.println("必须传入学生形态,才可以执行");}}
}
结果如下:
必须传入学生形态,才可以执行
锄禾日当午
六、Object类
6.1 概念
Object类是所有类的父类(基类),如果一个类没有明确的继承某一个具体的类,则将默认继承Object类。
例如我们定义了一个类:
public class Person {
}其实它被使用时是这样的:
public class Person extends Object {
}
观察Object类的构造器:只有一个无参构造方法。
6.2 Object的多态
由于Object是所有类的父类,使用Object可以接收任意的引用数据类型,观察下面的代码:
package com.kaikeba.objectoriented.senior;public class objectDemo {public static void main(String[] args) {String text = "123";say(text);int a = 10;say(a);}public static void say(Object o) {System.out.println(o);}
}
6.3 toString方法
Object类下有一些方法:
目前只要掌握toString和equals即可,而且可以查看java API中文手册,或者查看源码来了解方法的具体作用。
先来看一下toString的源码:
public String toString() {return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
返回对象的字符串表示形式,默认的是包名类名@哈希码 ,观察如下代码:
package com.kaikeba.objectoriented.senior;import java.util.Objects;public class Person7 {private String name;private int age;public Person7() {}public Person7(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}
}
package com.kaikeba.objectoriented.senior;public class toStringDemo {public static void main(String[] args) {Person7 p = new Person7("张三",18);//System.out.println方法中默认调用了toString方法System.out.println(p);}
}
输出的结果为:
com.kaikeba.objectoriented.senior.Person7@16e8e0a
这样对对象的描述一般不是很直观,所以一般建议继承的子类都要重写toString
方法,在Person7
类中加入重写方法:
@Overridepublic String toString() {return "Person7{" +"name='" + name + '\'' +", age=" + age +'}';}
输出结果就变为:
Person7{name='张三', age=18}
6.4 equals方法
指示某个其他对象是否“等于”此对象。
查看一下equals
的源码:
public boolean equals(Object obj) {return (this == obj);
}
默认的比较方式为 “==”,对于任何非空引用值x
和y
,当且仅当x
和y
引用同一对象时,此方法返回true
。
但是这样一般不太合理,一般而言,两个对象的每个属性都相同,那么我们就认为其相同,而不是非要两个引用都指向同一个对象才相同,所以一般建议重写Object类中的equals方法。
equals
方法重写的五个特性:(面试可能会问)
-
自反性 :对于任何非空的参考值x,x.equals(x)应该返回true;
-
对称性 :对于任何非空的引用值x和y,x.equals(y)应该返回true当且仅当y.equals(x)返回true;
-
传递性 :对于任何非空引用值x,y和z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)应该也返回true;
-
一致性 :对于任何非空引用值x和y,多次调用x.equals(y)始终返回true或始终返回false,前提是未修改对象上的equals比较中使用的信息;
-
非空性 :对于任何非空的参考值x,x.equals(null)应该返回false。
我们将上述的main函数修改如下:
package com.kaikeba.objectoriented.senior;public class toStringDemo {public static void main(String[] args) {Person7 p1 = new Person7("张三",18);System.out.println(p1);Person7 p2 = new Person7("张三",18);System.out.println(p2);System.out.println(p1.equals(p2));}
}
结果如下所示:
Person7{name='张三', age=18}
Person7{name='张三', age=18}
false
因为equals
默认的是使用了==来比较,p1和p2虽然属性值一样,但是毕竟是两个对象,地址值不一样,所以返回false;如果想通过属性值来比较两个对象是否相同,可以通过重写equals
方法来实现,在Person7
类中重写如下方法:
@Overridepublic boolean equals(Object o) {//如果两个引用指向一样,则肯定相同if (this == o) return true;//若o为空(能到这this肯定不为空)或this和o的类型不一致,肯定不同if (o == null || getClass() != o.getClass()) return false;Person7 person7 = (Person7) o;//类型一致,比较属性值是否一致return age == person7.age && Objects.equals(name, person7.name);}
或:
public boolean equals(Object o) {if(this == o) {return true;}if(o == null) {return fasle;}if(o instanceof Person7) {Person7 p2 = (Person7) o;if(this.name.equals(p2.name) && this.age == p2.age) {return true;} }return false;
}
在运行main函数,结果就变为true了,这也是比较合理的:
Person7{name='张三', age=18}
Person7{name='张三', age=18}
true
七、内部类
7.1 概念
在Java中,可以将**一个类定义在另一个类里面或者一个方法里面** ,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:
1. 成员内部类
2. 局部内部类
3. 匿名内部类
4. 静态内部类
7.2 成员内部类
成员内部类是最普通的内部类,它的定义为位于另一个类的内部,形如下面的形式:
class Outer {private double x = 0;public Outer(double x) {this.x = x;}class Inner { //内部类public void say() {System.out.println("x=" + x);}}
}
如何使用成员内部类:
Outer o = new Outer(); //先创建外部类对象
Outer.Inner i = o.new Inner(); //通过外部类对象来new内部类对象
特点:
成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:
外部类.this.成员变量
外部类.this.成员方法
看一个完整的例子:
package com.kaikeba.objectoriented.senior;public class Outer {private int x;public int getX() {return x;}public void setX(int x) {this.x = x;}class Inner {private int x = 200;public void say() {System.out.println("内部类的变量x的值:" + x);System.out.println("外部类的变量x的值:" + Outer.this.x);}}
}
package com.kaikeba.objectoriented.senior;public class memberInnerClass {public static void main(String[] args) {Outer o = new Outer();o.setX(100);Outer.Inner i = o.new Inner();i.say();}
}
结果如下:
内部类的变量x的值:200
外部类的变量x的值:100
7.3 局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或该作用域内。
看个例子直观感受一下:
package com.kaikeba.objectoriented.senior;public class localInnerClass {public static void main(String[] args) {class Person{public void say() {System.out.println("锄禾日当午");}}Person p = new Person();p.say();}
}
类的优点是复用性高,但是局部内部类出了方法或者作用域就不能用了,所以它不具备较高的复用性,使用的较少,但是创建一个只使用一次的对象可以考虑使用局部内部类:
package com.kaikeba.objectoriented.senior;public class localInnerClass2 {public static void main(String[] args) {class ChildImp implements Child {@Overridepublic void say() {System.out.println("新编写的局部内部类的say方法内部"+a);}}ChildImp c = new ChildImp();haha(c);}public static void haha(Child c) {}
}
注意:局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。
7.4 匿名内部类
匿名内部类由于没有名字,所以它的创建方式有点奇怪,创建格式如下:
new 父类构造器(参数列表) 实现接口()
{//匿名内部类的类体部分
}
在这里我们看到使用匿名内部类我们必须要继承一个父类或者实现一个接口,当然也仅能继承一个父类或者实现一个接口,同时它也是没有class
关键字,这是因为匿名内部类是直接使用new
来生成一个对象的引用,当然这个引用时隐式的。
package com.kaikeba.objectoriented.senior;public class anonymousInnerClass {public static void main(String[] args) {Child c = new Child() {@Overridepublic void say() {System.out.println();}};haha(c);}public static void haha(Child c) {}
}
注意:
-
使用匿名内部类是,我们必须继承一个类或者实现一个接口,但是两者不可兼得,同时也只能继承一个类或实现一个接口;
-
匿名内部类中是不能定义构造函数的;
-
匿名内部类中不能存在任何的静态成员变量和静态方法;
-
匿名内部类为局部内部类,所以局部内部类的所有限制同样对匿名内部类生效;
-
匿名内部类不能是抽象的,它必须要实现继承的类或者实现接口的所有抽象方法;
-
只能访问final型的局部变量 (面试题):因为内部类会被单独编译成一个字节码文件,为了保证这个单独的文件中用到的变量,与外面的变量的值绝对是一致的,系统从规则上限制了这个变量的值不可更改。
7.5 静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static
。
静态内部类是不需要依赖于外部类对象的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static
成员变量或者方法。
使用格式如下所示:
public class Test {public static void main(String[] args) {Outer.Inner inner = new Outer.Inner();}
}class Outer {public Outer() {}static class Inner {public Inner() {}}
}
八、包装类
8.1 概述
在java中有一个设计的原则“一切皆对象”,那么这样一来Java中的基本的数据类型,就完全不符合这种设计思想,因为Java中的八种基本数据类型并不是引用数据类型,所以Java中为了解决这样的问题,引入了**八种基本数据类型的包装类** 。
以上的八种包装类,可以将基本数据类型按照类的形式进行操作。也可以分为两种大的类型:
-
Number:Byte、Short、Integer、Long、Float、Double都是Number的子类表示是一个数字。
-
Object:Character、Boolean都是Object的直接子类。
包装类也是类,其在内存中的表示和一般的类一样:
8.2 装箱和拆箱操作
以下以Integer
和Float
为例进行操作:
-
装箱:将一个基本数据类型变为包装类;
-
拆箱:讲一个包装类变为一个基本数据类型;
看如下代码:
package com.kaikeba.objectoriented.senior;public class wrapperClass {public static void main(String[] args) {//手动装箱JDK1.5之前Integer i = new Integer(200);//手动拆箱JDK1.5之前int a = i.intValue();//自动装箱JDK1.5开始Integer j = 200;//自动拆箱JDK1.5开始int b = j;}
}
在JDK1.5之前,如果想要装箱,直接使用各个包装类的构造方法即可,例如:int temp = 10; //基本数据类型Integer x = new Integer(temp); //将基本数据类型变为包装类
在JDK1.5,Java新增了自动装箱和自动拆箱,可以直接通过包装类进行四则运算和自增自减操作,例如:Float f = 10.3f; //自动装箱float x = f; //自动拆箱System.out.println(f * f); //直接利用包装类完成System.out.println(x * x); //利用基本数据类型完成
因为所有的数值型的包装类都是Number
的子类,Number
的类中定义了如下的操作方法,以下的全部方法都是进行拆箱操作:
可以拆箱为不同的基本数据类型。
8.3 字符串转换
使用包装类还有一个很优秀的地方在于:可以将一个字符串变为指定的基本数据类型 ,此点一般在接收输入数据上使用较多。
在Integer类中提供了以下的操作方法:
public static int parseInt(String s)
:将String变为int型数据
在Float类中提供了以下的操作方法:
public static float parseFloat(String s)
:将String变为float型数据
…
观察如下代码:
package com.kaikeba.objectoriented.senior;import java.util.Scanner;public class wrapperClass2 {public static void main(String[] args) {Scanner input = new Scanner(System.in);System.out.println("请输入内容:");String text = input.nextLine();int x = Integer.parseInt(text);System.out.println(x+1);}
}
结果如下:
请输入内容:
100
101
nextLine
接收的是String
类型的输入,如果直接+1则当成字符串来操作了,需要用parseInt
转为int
类型,然后+1就是算术运算了。
九、可变参数
一个方法中定义完了参数,则在调用的时候必须传入与其一一对应的参数,但是在JDK1.5之后提供了新的功能,可以根据需要自动传入任意个数的参数 。
语法:返回值类型 方法名称(数据类型...参数名称) {//参数在方法内部,以数组的形式来接收}注意:可变参数只能出现在参数列表的最后。
package com.kaikeba.objectoriented.senior;public class variableParameters {public static void main(String[] args) {System.out.println(sum());System.out.println(sum(100));System.out.println(sum(100, 200));System.out.println(sum(100, 200, 300));System.out.println(sum(100, 200, 300, 500));}/*** int... nums,表示的是可变参数,调用时可以传递0-n个数字* 在方法的内部,可变参数以数组作为载体体现*/public static int sum(int... nums) {int n = 0;for(int i=0; i < nums.length; i++) {n += nums[i];}return n;}
}
结果如下所示:
0
100
300
600
1100