面向对象(Object Oriented)是软件开发方法,一种编程范式。把数据及对数据的操作方法放在一起,作为一个相互依存的整体——对象。对同类对象抽象出其共性,形成类。类中的大多数数据,只能用本类的方法进行处理。类通过一个简单的外部接口与外界发生关系,对象与对象之间通过消息进行通信。程序流程由用户在使用中决定。对象即为人对各种具体物体抽象后的一个概念,人们每天都要接触各种各样的对象,如手机就是一个对象。
项目名称 | 面向对象程序设计 | 面向过程程序设计(也叫结构化编程) |
---|---|---|
定义 | 面向对象顾名思义就是把现实中的事务都抽象成为程序设计中的“对象”,其基本思想是一切皆对象,是一种“自下而上”的设计语言,先设计组件,再完成拼装。 | 面向过程是“自上而下”的设计语言,先定好框架,再增砖添瓦。通俗点,就是先定好main()函数,然后再逐步实现mian()函数中所要用到的其他方法。 |
特点 | 封装、继承、多态 | 算法+数据结构 |
优势 | 适用于大型复杂系统,方便复用、 | 适用于简单系统,容易理解 |
劣势 | 比较抽象、性能比面向过程低 | 难以应对复杂系统,难以复用,不易维护、不易扩展 |
对比 | 易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护 | 性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。 |
设计语言 | Java、Smalltalk、EIFFEL、C++、Objective-、C#、Python等 | C、Fortran |
面向对象的基本特性
-
唯一:每个对象都有自身唯一的标识,通过这种标识,可找到相应的对象。在对象的整个生命期中,它的标识都不改变,不同的对象不能有相同的标识。
-
抽象:抽象性是指将具有一致的数据结构(属性)和行为(操作)的对象抽象成类。一个类就是这样一种抽象,它反映了与应用有关的重要性质,而忽略其他一些无关内容。任何类的划分都是主观的,但必须与具体的应用有关。
-
继承:继承性是子类自动共享父类数据结构和方法的机制,这是类之间的一种关系。在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容。
继承性是面向对象程序设计语言不同于其它语言的最重要的特点,是其他语言所没有的。
在类层次中,子类只继承一个父类的数据结构和方法,则称为单重继承。
在类层次中,子类继承了多个父类的数据结构和方法,则称为多重继承,Java仅支持单继承,C++支持均支持。
在软件开发中,类的继承性使所建立的软件具有开放性、可扩充性,这是信息组织与分类的行之有效的方法,它简化了对象、类的创建工作量,增加了代码的可重用性。
-
多态:多态性是指相同的操作或函数、过程可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果,这种现象称为多态性。
多态性允许每个对象以适合自身的方式去响应共同的消息。
多态性增强了软件的灵活性和重用性。
类与对象
类(Class)
用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。
格式:
访问修饰符 class 类名
{
特征
行为
}
// 注: 特征和行为都称为该类的成员;
一个业务类分为静态与动态部分,静态部分为成员变量,动态部分为方法
设计业务类时不能用main
方法
public class Student{
/** String name : 姓名 成员变量 */
String name;
/** int age : 年龄 成员变量 */
int age;
/** String sex : 性别 成员变量 */
String sex;
/**
* 动态部分:方法
*/
/** study是方法:成员方法 */
public void study(){
System.out.println("学习的方法");
}
/** exam是方法:成员方法 */
public void exam() {
System.out.println("考试的方法");
}
}
内部类
内部类是嵌套类的一个分类,即非静态嵌套类。内部类(非静态嵌套类)分为成员内部类、局部内部类和匿名内部类三种。
class OuterClass { // 外部类
// ...
class NestedClass { // 嵌套类,或称为内部类
// ...
}
}
非静态内部类是一个类中嵌套着另外一个类。 它有访问外部类成员的权限, 通常被称为内部类。
由于内部类嵌套在外部类中,因此必须首先实例化外部类,然后创建内部类的对象来实现。
内部类可以使用 private
或 protected
来修饰,如果不希望内部类被外部类访问可以使用 private
修饰符:
class OuterClass {
int x = 10;
private class InnerClass {
int y = 5;
}
}
public class MyMainClass {
public static void main(String[] args) {
OuterClass myOuter = new OuterClass();
// 此时由于内部类是private修饰,因此会报错
// OuterClass.InnerClass myInner = myOuter.new InnerClass();
// System.out.println(myInner.y + myOuter.x);
}
}
内部类可以访问外部类的属性和方法:
class OuterClass {
int x = 10;
class InnerClass {
public int myInnerMethod() {
return x;
}
}
}
public class MyMainClass {
public static void main(String[] args) {
OuterClass myOuter = new OuterClass();
OuterClass.InnerClass myInner = myOuter.new InnerClass();
System.out.println(myInner.myInnerMethod());
}
}
静态内部类
静态内部类可以使用 static
关键字定义,静态内部类不需要创建外部类来访问,可以直接访问,但静态内部类无法访问外部类的成员。
class OuterClass {
int x = 10;
static class InnerClass {
int y = 5;
}
}
public class MyMainClass {
public static void main(String[] args) {
OuterClass.InnerClass myInner = new OuterClass.InnerClass();
System.out.println(myInner.y);
}
}
匿名内部类
匿名内部类也就是没有名字的内部类,只能使用一次,抽象方法在new
对象时在大括号中实现,使用匿名内部类有个前提条件:必须继承一个父类或实现一个接口。
interface PersonInterface {
public void eat();
public void run();
}
abstract class PersonClass {
public abstract void eat();
}
public class Demo {
public static void main(String[] args) {
new PersonInterface(){
public void eat(){
System.out.println("eat something");
}
}.eat();
PersonClass p = new PersonClass() {
public void eat() {
System.out.println("eat something");
}
};
p.eat();
}
}
对象
对象是类的一个实例,有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。
创建对象:类名 对象名 = new 类名();
生命周期:对象从被创建开始–>当对象失去所有引用/对象真正的被销毀(对象会在堆里面占用内存,当把对象的内存空问回收了〕
匿名对象:就是创建的时候没有使用变量接收保存一个对象。但是匿名对象本质也是一个对象(具体的值)。在仅使用一次的情况,可以使用匿名对象。匿名对象可以节约资源,因为垃圾回收器(GC)会不定时回收匿名对象。
public class StudentTest {
public static void main(String[] args) {
// 创建对象
Student s1 = new Student(); // 用new创建一个对象,s1就是Student类的一个对象
String str = new Student(); // 匿名对象
System.out.println(s1);
System.out.println(new Student().name); // 匿名对象,并没有一个实际的对象名接收
// 为成员变量赋值:对象名.成员变量名 = 值
s1.name = "name1";
s1.age = 1;
s1.sex = "未知";
// 取值:对象名.成员变量名;
String stuName = s1.name;
System.out.println("学生姓名:" + stuName);
System.out.println("学生年龄" + s1.age);
System.out.println("学生性别" + s1.sex);
}
}
上述声明对象大致经过了一下三个内存运行过程:
-
进入
main
函数,在栈内创建main
栈帧; -
声明对象前读取
Student
类,在元空间查询是否有Student
类,没有则新建; -
声明对象,在堆内存创建新的空间存储
Student
的参数name
age
,并赋值,在main
栈帧内存储堆内对应参数存储地址;
了解对象
-
内存布局
在Hotspot虚拟机中一个对象的内存布局分为三个部分:对象头、实例数据、对齐填充。
-
对象头:
对象头又有两部分的信息:
第一部分是用于存储对象自身的运行数据(HashCode、GC分代年龄、锁状态标志等);
另一部分是类型指针,指向它的类元数据,虚拟机通过这个指针确定这个对象是哪个类的实例(如果使用句柄池方式则不会有)。
如果是数组还会有一个记录数组长度的如下表所示:
内容 说明 Mark Word 对象的hashCode或锁信息等 Class Metadata Address 对象类型数据指针 Array length 数组长度 -
实例数据:
实例数据部分是真正存储有效信息的部分,就是在代码中定义的各种类型的字段内容。无论是父类继承下来的,还是在子类中的。
-
对齐填充:
对齐填充不是必须存在的,仅仅起着占位符的作用,因为HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍。
-
-
对象的访问
Java程序中我们操作一个对象是通过指向这个对象的引用,分为以下两种方法:
-
直接指针法(HotSpot实现):引用中直接存储的就是堆中对象的地址(reference)。优点就是一次定位速度快,缺点是对象移动(GC时对象移动)引用本身需要修改。
-
句柄法:Java堆中划分出一部分作为句柄池,引用存储的是对象的句柄地址(reference),而句柄中包括了对象实例和类型的具体位置信息。优点是对象移动只会改变句柄中的实例数据指针,缺点是两次定位。
-
-
创建对象流程
-
当虚拟机遇到一条
new
指令时,会去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查代表的类是否已经被类加载器加载。如果没有被加载那么必须先执行这个类的加载。 -
类加载检查通过后,虚拟机将为新对象分配内存,对象所需内存的大小在类加载后便可以确定。
-
内存分配完成后,虚拟机需要将对象初始化为零值,保证对象的实例变量在代码中不赋初始值就能直接使用。类变量在类加载的准备阶段初始化为零值。
-
对对象头进行必要信息的设置,比如如何找到类的元数据信息、对象的HashCode、GC分代年龄等。
-
经过上述操作,一个新的对象已经产生,但是
<init>
方法还没有执行,所有的字段都是零值。这时候需要执行<init>
方法(构造方法)把对象按照程序员的意愿进行初始化。类变量的初始化操作在类加载的初始化阶段<init>
方法完成
分配内存有两种方式:
-
指针碰撞(Bump the Pointer)(默认用指针碰撞)
Java堆内存是规整的(使用标记整理或带压缩的垃圾收集器),使用一个指针指向空闲位置,分配内存既将指针移动与分配大小相等的距离。
-
空闲列表(Free List)
内存不是规整的(使用标记清除的垃圾收集器),虚拟机维护一个可用内存块列表,分配内存时从列表中找到一个足够大的内存空间划分给对象并更新可用内存列表。
注:无法找到足够的内存时会触发一次GC
分配内存时并发问题解决方案:
- CAS(compare and swap)对分配内存空间的动作进行同步处理,虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
- 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。
-
-
创建对象指令重排序问题
A a = new A();
new一个对象的简单分解动作:
-
分配对象的内存空间;
-
初始化对象;
-
设置引用指向分配的内存地址。
其中2、3两步间会发生指令重排序,导致多线程时如果在初始化之前访问对象则会出现问题,单例模式的双重检测锁模式正是会存在这个问题,可以使用volatile来禁止指令重排序解决问题。
-
构造方法
构造方法,是一种属于类的特殊方法,它是一个与类同名的方法。 对象的创建就是通过构造方法来完成,其功能主要是完成对象的初始化。 当类实例化一个对象时会自动调用构造方法。 构造方法和其他方法一样也可以重载。如果类里没有写构造方法,JVM会自动创建一个无参的构造方法,这个构造方法不会显示在类里面。
结构:
// 修饰符 方法名(与类名相同,包括大小写)
public class Student {
public Student(){
类变量a = 值;
}
}
调用构造方法:用 new
关键字调用,给变量赋值。
使用场景:
- 有参数的构造方法一般使用在参数较少的场景;
- 参数多时建议使用无参数的构造方法,多数情况应用无参数的构造方法。
注:
若构造函数内未初始化的类变量,JVM会默认初始化为变量类型的默认值(例如String类型的类变量未在构造函数中初始化,则该变量的默认值为null
);若在构造函数内新定义一个与类变量同名的变量,则在调用构造函数初始化时,不会初始化类变量,且构造函数内的局部变量也不会被引用。
public class Test {
int a;
int b;
public Test() {
int a = 99; // 新的局部变量,与类变量a无关
a = 2; // 会默认先识别为局部变量,不会初始化类变量
}
}
public class Demo {
public static void main(String[] args) {
Test test = new Test();
System.out.println(test.a); // 此时a未被初始化,默认值为0
System.out.println(test.b); // 构造函数内未初始化类变量b,默认初始值为0
}
}
封装
封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。
优点:
-
良好的封装能够减少耦合。
-
类内部的结构可以自由修改。
-
可以对成员变量进行更精确的控制。
-
隐藏信息,实现细节。
封装步骤:
-
私有化成员变量(一般限制为private)
public class Person { private String name; private int age; }
-
为每一个成员变量提供合理的
public
修饰的方法:例如:
getXxx()
方法,用于获取成员变量的值,setXxx(...)
方法设置成员变量的值。注:如果当前成员变量类型是boolean类型,将
getXxx()
改为isXxx()
。public class Person{ // 私有化成员变量 private String name; private int age; public int getAge(){ return age; } public String getName(){ return name; } public void setAge(int age){ this.age = age; } public void setName(String name){ this.name = name; } }
用
this
关键字解决实例变量private String name
和局部变量setName(String name)
中的name变量之间发生的同名的冲突。 -
该类必须用
public
修饰
继承
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
继承类型:
在 Java 中,继承可以使用 extends
和 implements
这两个关键字来实现继承,而且所有的类都是继承于 java.lang.Object,当一个类没有继承的两个关键字,则默认继承 Object。
public boolean equals(Object obj) {
return (this == obj); // 比较内存地址
}
// 分析:this当前对象(存放的内存地址) obj比较的对象(存放内存地址)
子类是不继承父类的构造器(构造方法或者构造函数)的,它只是调用(隐式或显式)。如果父类的构造器带有参数,则必须在子类的构造器中显式地通过 super
关键字调用父类的构造器并配以适当的参数列表。
如果父类构造器没有参数,则在子类的构造器中不需要使用 super
关键字调用父类构造器,系统会自动调用父类的无参构造器。
// 父类
class SuperClass {
private int n;
SuperClass(){
System.out.println("SuperClass()");
}
SuperClass(int n) {
System.out.println("SuperClass(int n)");
this.n = n;
}
}
// 子类1
class SubClass extends SuperClass{
private int n;
SubClass(){ // 自动调用父类的无参数构造器
System.out.println("SubClass");
}
public SubClass(int n){
super(300); // 调用父类中带有参数的构造器
System.out.println("SubClass(int n):"+n);
this.n = n;
}
}
// 子类2
class SubClass2 extends SuperClass{
private int n;
SubClass2(){
super(300); // 调用父类中带有参数的构造器
System.out.println("SubClass2");
}
public SubClass2(int n){ // 自动调用父类的无参数构造器
System.out.println("SubClass2(int n):"+n);
this.n = n;
}
}
// 测试类
public class TestSuperSub{
public static void main (String args[]){
System.out.println("------SubClass 类继承------");
SubClass sc1 = new SubClass();
SubClass sc2 = new SubClass(100);
System.out.println("------SubClass2 类继承------");
SubClass2 sc3 = new SubClass2();
SubClass2 sc4 = new SubClass2(200);
}
}
特点:
- 子类拥有父类非 private 的属性、方法。
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
- Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 B 类继承 A 类,C 类继承 B 类,所以按照关系就是 B 类是 C 类的父类,A 类是 B 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
- 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。
例:
// 父类 Person
public class Person {
private String name;
private String hobby;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getHobby() {
return hobby;
}
public void setHobby(String hobby) {
this.hobby = hobby;
}
public void hobby() {
System.out.println(name + "喜欢" + hobby);
}
}
// 子类 Student
public class Student extends Person{
private String classRoom;
public String getClassRoom() {
return classRoom;
}
public void setClassRoom(String classRoom) {
this.classRoom = classRoom;
}
public Student() {
super();
classRoom = "未知班级";
}
}
// 测试类 Test
public class Test {
public static void main(String[] args) {
Student stu1 = new Student();
stu1.setName("小周");
stu1.setHobby("睡大觉");
stu1.hobby();
stu1.setClassRoom("三年2班");
System.out.println(stu1.getName()+ " " +stu1.getClassRoom());
}
}
方法重载
重载(overloading) 是在同一个类里面,方法名相同,而参数不同(参数个数 / 类型 / 顺序不同)。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载。
方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单。
使用:
- 声明方法的时候,写不同的形参即可。
- 调用方法的时候,程序会根据参数数量、类型、顺序自动匹配调用。
重载规则:
-
被重载的方法必须改变参数列表(参数个数或类型不一样);
-
被重载的方法可以改变返回类型(方法重载的返回值类型通常都是相同的);
-
被重载的方法可以改变访问修饰符;
-
被重载的方法可以声明新的或更广的检查异常;
-
方法能够在同一个类中或者在一个子类中被重载;
-
无法以返回值类型作为重载函数的区分标准。
注:调用方法时如果匹配不上参数类型,会自动转型(例如long类型自动向double类型转换,即实参为long类型,可以匹配上形参为double类型的方法),若转型后仍无法匹配,则会报错。
public class Demo{
// 方法1,用于将两个int类型的数相加
public static int add(int a,int b) {
return a + b;
}
// 方法3,用于将三个double类型的数相加
public static double add(double a,double b,double c) {
return a + b + c;
}
public static void main(String[] args) {
System.out.println(add(1, 3)); // 调用第一个返回值为int类型的方法
System.out.println(add(1.6, 9.89, 11.3245)); // 调用第二个返回值为double类型的方法
}
}
方法重写
重写(override)是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写。
重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。但是重写方法也存在问题,不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。
例如: 父类的一个方法申明了一个检查异常 IOException
,但是在重写这个方法的时候不能抛出 Exception
异常,因为 Exception
是 IOException
的父类,抛出 IOException
异常或者 IOException
的子类异常。
重写规则:
- 参数列表与被重写方法的参数列表必须完全相同,返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类;
- 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为
public
,那么在子类中重写该方法就不能声明为protected
; - 父类的成员方法只能被它的子类重写,声明为
final
的方法不能被重写,声明为static
的方法不能被重写,但是能够被再次声明; - 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为
private
和final
的方法;子类和父类不在同一个包中,那么子类只能够重写父类的声明为public
和protected
的非 final 方法; - 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以;
- 构造方法不能被重写;不能继承一个类,则不能重写该类的方法。
class Animal{
public void move(){
System.out.println("动物跑动");
}
}
class Dog extends Animal{
public void move(){
System.out.println("狗跑动");
}
public void bark(){
System.out.println("狗叫");
}
}
public class DogTest{
public static void main(String args[]){
Animal a = new Animal(); // Animal 类型的 Animal 对象
Animal b = new Dog(); // Animal 类型的 Dog 对象
// 执行 Animal 类的方法
a.move();
//执行 Dog 类的方法
b.move();
// b.bark(); b的引用类型Animal没有bark方法,故会报错
}
}
抽象类
如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。
使用场景:
父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法。
定义方式:通过关键字 abstrct
定义类
// 抽象类
public abstract class Person{
//抽象方法
abstract void run();
}
// 继承子类
class Student extends Person{
//实现抽象方法
void run(){
System.out.println("跑步");
}
}
// 测试类
public class Demo{
public static void main(String args[]){
// 不关心Person变量的具体子类型,直接用父类抽象类声明变量类型:
Person stu = new Student();
stu.run();
}
}
这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
面向抽象编程的本质就是:
-
上层代码只定义规范,例如:
abstract class Person
; -
不需要子类就可以实现业务逻辑(正常编译);
-
具体的业务逻辑由不同的子类实现,调用者并不关心。
接口
接口(Interface),在JAVA中是一个抽象类型,是抽象方法的集合,接口通常以 interface
来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。
接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。
接口与类:
共同点:
- 一个接口可以有多个方法。
- 接口文件保存在 .java 结尾的文件中,文件名使用接口名;接口的字节码文件保存在 .class 结尾的文件中。
- 接口相应的字节码文件必须在与包名称相匹配的目录结构中。
区别:
- 接口不能用于实例化对象。
- 接口没有构造方法,其他所有的方法必须是抽象方法,Java 8 之后 接口中可以使用
default
和static
关键字修饰的非抽象方法,Java 9之后允许将方法定义为private
,使得某些复用的代码不会把方法暴露出去。 - 接口不能包含成员变量,除了
static
和final
变量。 - 接口不是被类继承了,而是要被类实现。
- 接口支持多继承。
接口特性:
- 接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为
public abstract
(只能是public abstract
,其他修饰符都会报错)。 - 接口中可以含有变量,但是接口中的变量会被隐式的指定为
public static final
变量(并且只能是public
,用private
修饰会报编译错误)。 - 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。
public interface 接口名称 [extends 其他的接口名] {
void eat();
// 默认方法只能在接口中使用
default sleep(){
// 方法体
}
static jump(){
// 方法体
}
}
抽象类与接口的区别:
- 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是
public static final
类型的。 - 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
接口的实现
当类实现接口的时候,类要实现接口中所有的方法。否则,类必须声明为抽象的类。
类使用implements
关键字实现接口。在类声明中,implements
关键字放在class声明后面。
类名 implements 接口名1, 接口名2, 接口3, ...]
public class MammalInt implements Animal{
public void eat(){
System.out.println("Mammal eats");
}
public void travel(){
System.out.println("Mammal travels");
}
public int noOfLegs(){
return 0;
}
public static void main(String args[]){
MammalInt m = new MammalInt();
m.eat();
m.travel();
}
}
重写接口中声明的方法时的规则:
- 类在实现接口的方法时,不能抛出强制性异常,只能在接口中,或者继承接口的抽象类中抛出该强制性异常。
- 类在重写方法时要保持一致的方法名,并且应该保持相同或者相兼容的返回值类型。
- 如果实现接口的类是抽象类,那么就没必要实现该接口的方法。
接口的继承
在Java中,类的多继承是不合法,但接口允许多继承。接口的继承使用extends
关键字,子接口继承父接口的方法。public interface 接口 extends 接口1, 接口2
// Sports接口声明了两个方法
public interface Sports
{
public void setHomeTeam(String name);
public void setVisitingTeam(String name);
}
// Football接口自己声明了三个方法 继承于 Sports接口,从Sports接口继承了两个方法
// 实现Football接口的类需要实现五个方法
public interface Football extends Sports
{
public void homeTeamScored(int points);
public void visitingTeamScored(int points);
public void endOfQuarter(int quarter);
}
// Hockey接口自己声明了四个方法,继承于 Sports接口,从Sports接口继承了两个方法
// 实现Hockey接口的类需要实现六个方法
public interface Hockey extends Sports
{
public void homeGoalScored();
public void visitingGoalScored();
public void endOfPeriod(int period);
public void overtimePeriod(int ot);
}
标记接口
标记接口是没有任何方法和属性的接口,它仅仅表明它的类属于一个特定的类型,供其他代码来测试允许做一些事情。
public interface EventListener
{}
标记接口主要用于以下两种目的:
-
建立一个公共的父接口:
正如
EventListener
接口,这是由几十个其他接口扩展的Java API,你可以使用一个标记接口来建立一组接口的父接口。例如:当一个接口继承了EventListener接口,Java虚拟机(JVM)就知道该接口将要被用于一个事件的代理方案。 -
向一个类添加数据类型:
这种情况是标记接口最初的目的,实现标记接口的类不需要定义任何接口方法(因为标记接口根本就没有方法),但是该类通过多态性变成一个接口类型。
函数式接口
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法(继承自Object
类的方法不计数,默认方法和静态方法也不计数),但是可以有多个非抽象方法的接口。JDK1.8之后函数式接口可以被隐式转换为 lambda
表达式。可以使用注解@FunctionalInterface
标记该接口为函数式接口。
Lambda
表达式和方法引用:
// 定义一个函数式接口
@FunctionalInterface
interface GreetingService
{
void sayMessage(String message);
}
// 使用Lambda表达式来表示该接口的一个实现
GreetingService greetService1 = message -> System.out.println("Hello " + message);
JDK 1.8 之前已有的函数式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.nio.file.PathMatcher
- java.lang.reflect.InvocationHandler
- java.beans.PropertyChangeListener
- java.awt.event.ActionListener
- javax.swing.event.ChangeListener
JDK 1.8 新增加的函数接口:
- java.util.function
Lambda表达式
可以看成是对匿名内部类的简写,使用Lambda表达式时,接口必须是函数式接口。
语法:
// (参数1,参数2…)表示参数列表;
// ->表示连接符;
// {}内部是方法体
<函数式接口> <变量名> = (参数1,参数2...) -> {
//方法体
}
特点说明:
=
右边的类型会根据左边的函数式接口类型自动推断;- 如果形参列表为空,只需保留
()
; - 如果形参只有1个,
()
可以省略,只需要参数的名称即可; - 如果执行语句只有1句,且无返回值,
{}
可以省略,若有返回值,则若想省去{}
,则必须同时省略return
,且执行语句也保证只有1句; - 形参列表的数据类型会自动推断;
- lambda不会生成一个单独的内部类文件;
- lambda表达式若访问了局部变量,则局部变量必须是
final
的,若是局部变量没有加final
关键字,系统会自动添加,此后在修改该局部变量,会报错;
// 函数式接口
public interface MyInterface1{
int sum(int num1,int num2);
}
public interface MyInterface2{
void print(String str);
}
public interface MyInterface3{
void test();
}
// 测试类
// 匿名内部类
public class Test1{
public static void main(String[] args) {
MyInterface1 mf = new MyInterface() {
@Override
public int sum(int num1, int num2) {
return num1+num2;
}
};
System.out.println(mf.sum(10, 20));
}
}
// Lambda表达式
// 两个参数有返回值
public class Test2{
public static void main(String[] args) {
// 常规写法
MyInterface1 mf = (int num1,int num2)->{
return num1+num2;
};
// 简写
MyInterface1 mf1 = (num1,num2)->num1+num2;
System.out.println(mf1.sum(10, 12));
}
}
// 一个参数没有返回值
public class Test3{
public static void main(String[] args) {
// 常规写法
MyInterface mi = (String str)->{
System.out.println(str);
};
// 简写
MyInterface mi1 = str->System.out.println(str);
mi1.print("Hello Lambda");
}
}
// 没有参数
public class Test4{
public static void main(String[] args) {
// 常规写法
MyInterface mi = ()->{
System.out.println("123456");
};
// 简写,大括号不写代码没有什么意义
MyInterface mi2 = ()->{};
}
}
Lambda表达式的方法引用
-
构造方法引用:
public class Person { private int id; private String name; public Person(int id, String name) { this.id = id; this.name = name; } public Person() { } @Override public String toString() { return "Student [id=" + id + ", name=" + name + "]"; } } public interface PersonFactory{ Person creatPerson(int id,String name); } public static void main(String[] args) { PersonFactory factory = new PersonFactory() { @Override public Person creatPerson(int id, String name) { return new Person(11,"小猪佩奇"); } }; // 简写1 PersonFactory factory1 = (id,name)->new Person(id,name); // 简写2 PersonFactory factory2 = Person::new; }
-
静态方法引用:
public interface PersonFactory{ int parse(String str); } public static void main(String[] args) { PersonFactory pf = new PersonFactory() { @Override public int parse(String str) { // TODO Auto-generated method stub return Integer.parseInt(str); } }; // 简写1 PersonFactory factory = str->Integer.parseInt(str); // 简写2:当有返回值,且类型与调用的静态方法返回值相同,参数列表也一致,则可以将简写1的格式改为简写2的格式 PersonFactory fp = Integer::parseInt; }
-
实例方法引用:
Java1.8提供了一个断言型函数式接口Function,接受两个参数。
@FunctionalInterface public interface Function<T, R> { /** * Applies this function to the given argument. * * @param t the function argument * @return the function result */ R apply(T t);
public static void main(String[] args) { String str = "hello.itsource"; //匿名内部类的方式 Function<String,Boolean> func1 = new Function<String, Boolean>() { @Override public Boolean apply(String t) { return t.endsWith("itsource"); } }; boolean test = test(func1,str); System.out.println(test); } public static boolean test(Function<String,Boolean> f,String str){ return f.apply(str); } // Lambda常规写法 public static void main(String[] args) { String str = "hello.itsource"; //匿名内部类的方式 Function<String,Boolean> func1 = t->str.endsWith("itsource"); boolean test = test(func1,str); System.out.println(test); } public static boolean test(Function<String,Boolean> f,String str){ return f.apply(str); } // Lambda简写 public static void main(String[] args) { String str = "hello.itsource"; //匿名内部类的方式 Function<String,Boolean> func1 = str::endsWith; boolean test = test(func1,str); System.out.println(test); } public static boolean test(Function<String,Boolean> f,String str){ return f.apply(str); }
多态
多态(polymorphism)是同一个行为具有多个不同表现形式或形态的能力,例如多态是同一个接口,使用不同的实例而执行不同操作,可以创建子类对象保存在父类变量中。
Animal a1 = new Person();
// 编译时类型为Animal
// 运行时类型为new Person();
作用:
- 屏蔽子类中特有成员 (成员变量、成员方法),成员变量没有多态性;
- 扩展父类的功能,子类重写父类方法,调用重写后的方法;
- 声明通用方法。
public class Animal {
int a = 1;
public void eat() {
System.out.println("Animal吃");
}
}
public class Person extends Animal{
int a = 2;
@Override
public void eat() {
System.out.println("Person吃");
}
}
public class Pig extends Animal{
int a = 2;
@Override
public void eat() {
System.out.println("Pig吃");
}
}
public class ActionData {
void add(Animal Animal) {
System.out.println("添加Animal的方法");
}
}
public class Test {
public static void main(String[] args) {
Animal a1 = new Person(); // 向上转型(造型)
System.out.println(a1.a); // 结果为1,a1是Animal类型
a1.eat(); // Persong 吃,调用重写后的方法
Person person = new Person();
ActionData ad = new ActionData();
ad.add(person); // 将person向上造型
}
}
使用语法:
-
体现多态性,向上造型
父类 变量名 = new 子类();
-
调用子类特有成员,向下造型,需要用
instanceof
判断是否包含子类对象子类 变量名 = (子类)父类对象
Animal animal = new Pig(); // Pig类型 向上造型 为 Animal类型
Person person = (Person)animal; // Animal类型 向下造型 为 Person类
//person.eat(); // 调用子类方法
// 此时会报错java.lang.ClassCastException(类型转换异常),是因为把Pig类型变换为无继承关系的Person类型
if(animal instanceof Person){
person.eat();
}
继承关系的构造方法执行流程
每个类都是先加载静态代码块,再加载构造代码块,再执行构造方法。根据类的关系从最高父类开始执行,逐级向下执行,直到当前类的构造方法结束
枚举
枚举是一个特殊的类,是一个被命名的整型常数的集合,用于声明一组带标识符的常数,枚举类可以声明在内部类,使用 enum
关键字来定义,访问通过 枚举类名.常数名
实现。
enum Color
{
RED, GREEN, BLUE; // 为Color的实例,等价于调用无参构造方法
}
public class Test
{
// 执行输出结果
public static void main(String[] args)
{
Color c1 = Color.RED;
System.out.println(c1); // 结果为:RED
}
}
每个枚举都是通过 Class 在内部实现的,且所有的枚举值都是 public static final
的。
以上的枚举类 Color 转化在内部类实现:
class Color
{
public static final Color RED = new Color(); // 默认值为自己,即RED
public static final Color BLUE = new Color();
public static final Color GREEN = new Color();
}
迭代枚举元素
-
for循环进行枚举:
enum Color { RED, GREEN, BLUE; } public class MyClass { public static void main(String[] args) { for (Color myVar : Color.values()) { System.out.println(myVar); } } }
-
switch进行枚举
enum Color { RED, GREEN, BLUE; } public class MyClass { public static void main(String[] args) { Color myVar = Color.BLUE; switch(myVar) { case RED: System.out.println("红色"); break; case GREEN: System.out.println("绿色"); break; case BLUE: System.out.println("蓝色"); break; } } }
常用方法
enum
定义的枚举类默认继承了 java.lang.Enum
类,并实现了 java.lang.Serializable
和 java.lang.Comparable
两个接口。
-
values()
返回枚举类中所有的值。 -
ordinal()
方法可以找到每个枚举常量的索引,就像数组索引一样。 -
valueOf()
方法返回指定字符串值的枚举常量。
enum Color{
RED, GREEN, BLUE;
}
public class Test{
public static void main(String[] args){
// 调用 values()
Color[] arr = Color.values();
// 迭代枚举
for (Color col : arr){
// 查看索引
System.out.println(col + " at index " + col.ordinal());
}
// 使用 valueOf() 返回枚举常量,不存在的会报错 IllegalArgumentException
System.out.println(Color.valueOf("RED"));
// System.out.println(Color.valueOf("WHITE")); 会报错
}
}
/*
* 输出结果:
* RED at index 0
* GREEN at index 1
* BLUE at index 2
* RED
*/
枚举类成员
枚举跟普通类一样可以用自己的变量、方法和构造函数,构造函数只能使用 private
访问修饰符,所以外部无法调用。
枚举既可以包含具体方法,也可以包含抽象方法。 如果枚举类具有抽象方法,则枚举类的每个实例都必须实现它。
enum Color{
RED{
public String getColor(){ //枚举对象实现抽象方法
return "红色";
}
},
GREEN{
public String getColor(){ //枚举对象实现抽象方法
return "绿色";
}
},
BLUE{
public String getColor(){ //枚举对象实现抽象方法
return "蓝色";
}
};
// 构造函数
private Color()
{
System.out.println("Constructor called for : " + this.toString());
}
public abstract String getColor(); // 定义抽象方法
public void colorInfo()
{
System.out.println("Universal Color");
}
}
public class Test
{
// 输出
public static void main(String[] args){
for (Color c:Color.values()){
System.out.println(c.getColor() + " ");
}
Color c1 = Color.RED;
System.out.println(c1);
c1.colorInfo();
}
}
/*
* 输出结果:
* Constructor called for : RED
* Constructor called for : GREEN
* Constructor called for : BLUE
* 红色
* 绿色
* 蓝色
* RED
* Universal Color
*/