Java基础复习(二):面向对象编程
访问权限修饰符
- 默认访问权限(包访问权限):用来修饰类的话,表示该类只对同一个包中的其他类可见。
- public:用来修饰类的话,表示该类对其他所有的类都可见。
- 默认访问权限(包访问权限):如果一个类的方法或变量被包访问权限修饰,也就意味着只能在同一个包中的其他类中显示地调用该类的方法或者变量,在不同包中的类中不能显式地调用该类的方法或变量。
- private:如果一个类的方法或者变量被 private 修饰,那么这个类的方法或者变量只能在该类本身中被访问,在类外以及其他类中都不能显式的进行访问。
- protected:如果一个类的方法或者变量被 protected 修饰,对于同一个包的类,这个类的方法或变量是可以被访问的。对于不同包的类,只有继承于该类的类才可以访问到该类的方法或者变量。
- public:被 public 修饰的方法或者变量,在任何地方都是可见的。
补充一些关于 Java 包和类文件的知识:
1)Java 中的包主要是为了防止类文件命名冲突以及方便进行代码组织和管理;
2)对于一个 Java 源代码文件,如果存在 public 类的话,只能有一个 public 类,且此时源代码文件的名称必须和 public 类的名称完全相同。
另外,如果还存在其他类,这些类在包外是不可见的。如果源代码文件没有 public 类,则源代码文件的名称可以随意命名。
Java代码初始化块:了解实例初始化和静态初始化的过程
对于代码初始化来说,它有三个规则
- 类实例化的时候执行代码初始化块;
- 实际上,代码初始化块是放在构造方法中执行的,只不过比较靠前;
- 代码初始化块里的执行顺序是从前到后的。
静态初始化块在类加载时执行,只会执行一次,并且优先于实例初始化块和构造方法的执行;实例初始化块在每次创建对象时执行,在构造方法之前执行
抽象类
定义抽象类的时候需要用到关键字 abstract
,放在 class
关键字前,就像下面这样。
abstract class AbstractPlayer {
}
抽象类是不能实例化的,尝试通过 new
关键字实例化的话,编译器会报错,提示“类是抽象的,不能实例化”。
虽然抽象类不能实例化,但可以有子类。子类通过 extends
关键字来继承抽象类。
如果一个类定义了一个或多个抽象方法,那么这个类必须是抽象类。
抽象类中既可以定义抽象方法,也可以定义普通方法
抽象类派生的子类必须实现父类中定义的抽象方法。
- 1、抽象类不能被实例化。
- 2、抽象类应该至少有一个抽象方法,否则它没有任何意义。
- 3、抽象类中的抽象方法没有方法体。
- 4、抽象类的子类必须给出父类中的抽象方法的具体实现,除非该子类也是抽象类。
接口
接口中定义静态方法的目的是为了提供一种简单的机制,使我们不必创建对象就能调用方法,从而提高接口的竞争力。
- 接口中允许定义变量
- 接口中允许定义抽象方法
- 接口中允许定义静态方法(Java 8 之后)
- 接口中允许定义默认方法(Java 8 之后)
1)接口不允许直接实例化,否则编译器会报错。
需要定义一个类去实现接口,见下例。
public class Computer implements Electronic {
public static void main(String[] args) {
new Computer();
}
@Override
public int getElectricityUse() {
return 0;
}
}
然后再实例化。
Electronic e = new Computer();
2)接口可以是空的,既可以不定义变量,也可以不定义方法。
public interface Serializable {
}
Serializable 接口用来为序列化的具体实现提供一个标记,也就是说,只要某个类实现了 Serializable 接口,那么它就可以用来序列化了。
3)不要在定义接口的时候使用 final 关键字,否则会报编译错误,因为接口就是为了让子类实现的,而 final 阻止了这种行为。
4)接口的抽象方法不能是 private、protected 或者 final,否则编译器都会报错。
5)接口的变量是隐式 public static final
(常量),所以其值无法改变。
接口的作用
第一,使某些实现类具有我们想要的功能
第二,Java 原则上只支持单一继承,但通过接口可以实现多重继承的目的。
第三,实现多态。
多态存在的 3 个前提:
- 1、要有继承关系,比如说 Circle 和 Square 都实现了 Shape 接口。
- 2、子类要重写父类的方法,Circle 和 Square 都重写了
name()
方法。 - 3、父类引用指向子类对象,circleShape 和 squareShape 的类型都为 Shape,但前者指向的是 Circle 对象,后者指向的是 Square 对象。
接口的三种模式
策略模式、适配器模式和工厂模式。
策略模式的思想是,针对一组算法,将每一种算法封装到具有共同接口的实现类中,接口的设计者可以在不影响调用者的情况下对算法做出改变。
适配器模式的思想是,针对调用者的需求对原有的接口进行转接。生活当中最常见的适配器就是HDMI(英语:High Definition Multimedia Interface
,中文:高清多媒体接口)线,可以同时发送音频和视频信号。
所谓的工厂模式理解起来也不难,就是什么工厂生产什么,比如说宝马工厂生产宝马,奔驰工厂生产奔驰,A 级学院毕业 A 级教练,C 级学院毕业 C 级教练。
抽象类和接口的区别
- 1、抽象类可以有方法体的方法,但接口没有(Java 8 以前)。
- 2、接口中的成员变量隐式为
static final
,但抽象类不是的。 - 3、一个类可以实现多个接口,但只能继承一个抽象类。
- 接口是隐式抽象的,所以声明时没有必要使用
abstract
关键字; - 接口的每个方法都是隐式抽象的,所以同样不需要使用
abstract
关键字; - 接口中的方法都是隐式
public
的。
1)语法层面上
- 抽象类可以包含具体方法的实现;而在接口中,方法默认是 public abstract 的,但从 Java 8 开始,接口也可以包含有实现的默认方法和静态方法。
- 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的;
- 接口中不能含有静态代码块,而抽象类可以有静态代码块;
- 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
2)设计层面上
抽象类是对一种事物的抽象,即对类抽象,继承抽象类的子类和抽象类本身是一种 is-a
的关系。而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类 Airplane,将鸟设计为一个类 Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。
此时可以将 飞行 设计为一个接口 Fly,包含方法 fly(),然后 Airplane 和 Bird 分别根据自己的需要实现 Fly 这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承 Airplane 即可,对于鸟也是类似的,不同种类的鸟直接继承 Bird 类即可。从这里可以看出,继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。
接口是对类的某种行为的一种抽象,接口和类之间并没有很强的关联关系,举个例子来说,所有的类都可以实现 Serializable接口,从而具有序列化的功能,但不能说所有的类和 Serializable 之间是 is-a
的关系。
抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。什么是模板式设计?最简单例子,大家都用过 ppt 里面的模板,如果用模板 A 设计了 ppt B 和 ppt C,ppt B 和 ppt C 公共的部分就是模板 A 了,如果它们的公共部分需要改动,则只需要改动模板 A 就可以了,不需要重新对 ppt B 和 ppt C 进行改动。而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。
内部类
成员内部类
内部类可以随心所欲地访问外部类的成员,但外部类想要访问内部类的成员,就不那么容易了,必须先创建一个成员内部类的对象,再通过这个对象来访问
这也就意味着,如果想要在静态方法中访问成员内部类的时候,就必须先得创建一个外部类的对象,因为内部类是依附于外部类的。
这种创建内部类的方式在实际开发中并不常用,因为内部类和外部类紧紧地绑定在一起,使用起来非常不便。
局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,所以局部内部类的生命周期仅限于作用域内。
局部内部类就好像一个局部变量一样,它是不能被权限修饰符修饰的,比如说 public、protected、private 和 static 等。
匿名内部类
匿名内部类是我们平常用得最多的,尤其是启动多线程的时候,会经常用到,并且 IDE 也会帮我们自动生成。
匿名内部类就好像一个方法的参数一样,用完就没了,以至于我们都不需要为它专门写一个构造方法,它的名字也是由系统自动命名的。仔细观察编译后的字节码文件也可以发现,匿名内部类连名字都不配拥有,哈哈,直接借用的外部类,然后 $1
就搞定了。
匿名内部类是唯一一种没有构造方法的类。就上面的写法来说,匿名内部类也不允许我们为其编写构造方法,因为它就像是直接通过 new 关键字创建出来的一个对象。
匿名内部类的作用主要是用来继承其他类或者实现接口,并不需要增加额外的方法,方便对继承的方法进行实现或者重写。
静态内部类
静态内部类和成员内部类类似,只是多了一个 static 关键字。
由于 static 关键字的存在,静态内部类是不允许访问外部类中非 static 的变量和方法的,这一点也非常好理解:你一个静态的内部类访问我非静态的成员变量干嘛?
为什么要使用内部类呢?
使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
使用内部类还能够为我们带来如下特性:
- 1、内部类可以使用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
- 2、在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
- 3、创建内部类对象的时刻并不依赖于外部类对象的创建。
- 4、内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
- 5、内部类提供了更好的封装,除了该外围类,其他类都不能访问。
深入理解Java三大特性:封装、继承和多态
1)封装
封装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。
数据被保护在类的内部,尽可能地隐藏内部的实现细节,只保留一些对外接口使之与外部发生联系。
其他对象只能通过已经授权的操作来与这个封装的对象进行交互。也就是说用户是无需知道对象内部的细节(当然也无从知道),但可以通过该对象对外的提供的接口来访问该对象。
使用封装有 4 大好处:
- 1、良好的封装能够减少耦合。
- 2、类内部的结构可以自由修改。
- 3、可以对成员进行更精确的控制。
- 4、隐藏信息,实现细节。
2)继承
继承就是子类继承父类的属性和方法,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的方法。
继承分为单继承和多继承,Java 语言只支持类的单继承,但可以通过实现接口的方式达到多继承的目的。
单继承
单继承,一个子类只有一个父类,如我们上面讲过的 Animal 类和它的子类。单继承在类层次结构上比较清晰,但缺点是结构的丰富度有时不能满足使用需求。
多继承
多继承,一个子类有多个直接的父类。这样做的好处是子类拥有所有父类的特征,子类的丰富度很高,但是缺点就是容易造成混乱。
Java 虽然不支持多继承,但是 Java 有三种实现多继承效果的方式,分别是内部类、多层继承和实现接口。
内部类可以继承一个与外部类无关的类,保证了内部类的独立性,正是基于这一点,可以达到多继承的效果。
多层继承:子类继承父类,父类如果还继承其他的类,那么这就叫多层继承。这样子类就会拥有所有被继承类的属性和方法。
实现接口无疑是满足多继承使用需求的最好方式,一个类可以实现多个接口满足自己在丰富性和复杂环境的使用需求。
类和接口相比,类就是一个实体,有属性和方法,而接口更倾向于一组方法。
子类继承父类后,就拥有父类的非私有的属性和方法。
this 和 super 关键字
this 和 super 关键字是继承中非常重要的知识点,分别表示当前对象的引用和父类对象的引用,两者有很大相似又有一些区别。
this 表示当前对象,是指向自己的引用。
this.属性 // 调用成员变量,要区别成员变量和局部变量
this.() // 调用本类的某个方法
this() // 表示调用本类构造方法
super 表示父类对象,是指向父类的引用。
super.属性 // 表示父类对象中的成员变量
super.方法() // 表示父类对象中定义的方法
super() // 表示调用父类构造方法
构造方法
构造方法是一种特殊的方法,它是一个与类同名的方法。在继承中构造方法是一种比较特殊的方法(比如不能继承),所以要了解和学习在继承中构造方法的规则和要求。
继承中的构造方法有以下几点需要注意:
父类的构造方法不能被继承:
因为构造方法语法是与类同名,而继承则不更改方法名,如果子类继承父类的构造方法,那明显与构造方法的语法冲突了。比如 Father 类的构造方法名为 Father(),Son 类如果继承 Father 类的构造方法 Father(),那就和构造方法定义:构造方法与类同名冲突了,所以在子类中不能继承父类的构造方法,但子类会调用父类的构造方法。
子类的构造过程必须调用其父类的构造方法:
Java 虚拟机构造子类对象前会先构造父类对象,父类对象构造完成之后再来构造子类特有的属性,这被称为内存叠加。而 Java 虚拟机构造父类对象会执行父类的构造方法,所以子类构造方法必须调用 super()即父类的构造方法。就比如一个简单的继承案例应该这么写:
class A{
public String name;
public A() {//无参构造
}
public A (String name){//有参构造
}
}
class B extends A{
public B() {//无参构造
super();
}
public B(String name) {//有参构造
//super();
super(name);
}
}
如果子类的构造方法中没有显示地调用父类构造方法,则系统默认调用父类无参数的构造方法。
方法重写(Override)
方法重写也就是子类中出现和父类中一模一样的方法(包括返回值类型,方法名,参数列表),它建立在继承的基础上。你可以理解为方法的外壳不变,但是核心内容重写。
在这里提供一个简单易懂的方法重写案例:
class E1{
public void doA(int a){
System.out.println("这是父类的方法");
}
}
class E2 extends E1{
@Override
public void doA(int a) {
System.out.println("我重写父类方法,这是子类的方法");
}
}
其中@Override
注解显示声明该方法为注解方法,可以帮你检查重写方法的语法正确性,当然如果不加也是可以的,但建议加上。
方法重载(Overload)
如果有两个方法的方法名相同,但参数不一致,那么可以说一个方法是另一个方法的重载。
重载可以通常理解为完成同一个事情的方法名相同,但是参数列表不同其他条件也可能不同。一个简单的方法重载的例子,类 E3 中的 add()方法就是一个重载方法。
class E3{
public int add(int a,int b){
return a+b;
}
public double add(double a,double b) {
return a+b;
}
public int add(int a,int b,int c) {
return a+b+c;
}
}
继承与修饰符
Java 修饰符的作用就是对类或类成员进行修饰或限制,每个修饰符都有自己的作用,而在继承中可能有些特殊修饰符使得被修饰的属性或方法不能被继承,或者继承需要一些其他的条件。
Java 语言提供了很多修饰符,修饰符用来定义类、方法或者变量,通常放在语句的最前端。主要分为以下两类:
- 访问权限修饰符,也就是 public、private、protected 等
- 非访问修饰符,也就是 static、final、abstract 等
访问修饰符
Java 子类重写继承的方法时,不可以降低方法的访问权限,子类继承父类的访问修饰符作用域不能比父类小,也就是更加开放,假如父类是 protected 修饰的,其子类只能是 protected 或者 public,绝对不能是 default(默认的访问范围)或者 private。所以在继承中需要重写的方法不能使用 private 修饰词修饰。
继承当中子类抛出的异常必须是父类抛出的异常或父类抛出异常的子异常。
非访问修饰符
访问修饰符用来控制访问权限,而非访问修饰符每个都有各自的作用,下面针对 static、final、abstract 修饰符进行介绍。
static 修饰符
static 翻译为“静态的”,能够与变量,方法和类一起使用,称为静态变量,静态方法(也称为类变量、类方法)。如果在一个类中使用 static 修饰变量或者方法的话,它们可以直接通过类访问,不需要创建一个类的对象来访问成员。
final 修饰符
final 变量:
- final 表示"最后的、最终的"含义,变量一旦赋值后,不能被重新赋值。被 final 修饰的实例变量必须显式指定初始值(即不能只声明)。final 修饰符通常和 static 修饰符一起使用来创建类常量。
final 方法:
- 父类中的 final 方法可以被子类继承,但是不能被子类重写。声明 final 方法的主要目的是防止该方法的内容被修改。
final 类:
- final 类不能被继承,没有类能够继承 final 类的任何特性。
所以无论是变量、方法还是类被 final 修饰之后,都有代表最终、最后的意思。内容无法被修改。
abstract 修饰符
abstract 英文名为“抽象的”,主要用来修饰类和方法,称为抽象类和抽象方法。
抽象方法:有很多不同类的方法是相似的,但是具体内容又不太一样,所以我们只能抽取他的声明,没有具体的方法体,即抽象方法可以表达概念但无法具体实现。
抽象类:有抽象方法的类必须是抽象类,抽象类可以表达概念但是无法构造实体的类。
Object 类和转型
提到 Java 继承,不得不提及所有类的根类:Object(java.lang.Object)类,如果一个类没有显式声明它的父类(即没有写 extends xx),那么默认这个类的父类就是 Object 类,任何类都可以使用 Object 类的方法,创建的类也可和 Object 进行向上、向下转型,所以 Object 类是掌握和理解继承所必须的知识点。
Java 向上和向下转型在 Java 中运用很多,也是建立在继承的基础上,所以 Java 转型也是掌握和理解继承所必须的知识点。
Object 类概述
- Object 是类层次结构的根类,所有的类都隐式的继承自 Object 类。
- Java 中,所有的对象都拥有 Object 的默认方法。
- Object 类有一个构造方法,并且是无参构造方法。
Object 是 Java 所有类的父类,是整个类继承结构的顶端,也是最抽象的一个类。
像 toString()、equals()、hashCode()、wait()、notify()、getClass()等都是 Object 的方法。你以后可能会经常碰到,但其中遇到更多的就是 toString()方法和 equals()方法,我们经常需要重写这两种方法满足我们的使用需求。
toString()方法表示返回该对象的字符串,由于各个对象构造不同所以需要重写,如果不重写的话默认返回类名@hashCode
格式。
如果重写 toString()方法后直接调用 toString()方法就可以返回我们自定义的该类转成字符串类型的内容输出,而不需要每次都手动的拼凑成字符串内容输出,大大简化输出操作。
equals()方法主要比较两个对象是否相等,因为对象的相等不一定非要严格要求两个对象地址上的相同,有时内容上的相同我们就会认为它相等,比如 String 类就重写了 euqals()方法,通过字符串的内容比较是否相等。
向上转型
向上转型 : 通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换
向下转型
向下转型 : 通过父类对象(大范围)实例化子类对象(小范围),在书写上父类对象需要加括号()
强制转换为子类类型。但父类引用变量实际引用必须是子类对象才能成功转型
子父类初始化顺序
在 Java 继承中,父子类初始化先后顺序为:
- 父类中静态成员变量和静态代码块
- 子类中静态成员变量和静态代码块
- 父类中普通成员变量和代码块,父类的构造方法
- 子类中普通成员变量和代码块,子类的构造方法
总的来说,就是静态>非静态,父类>子类,非构造方法>构造方法。
多态
Java 的多态是指在面向对象编程中,同一个类的对象在不同情况下表现出来的不同行为和状态。
- 子类可以继承父类的字段和方法,子类对象可以直接使用父类中的方法和字段(私有的不行)。
- 子类可以重写从父类继承来的方法,使得子类对象调用这个方法时表现出不同的行为。
- 可以将子类对象赋给父类类型的引用,这样就可以通过父类类型的引用调用子类中重写的方法,实现多态。
多态的目的是为了提高代码的灵活性和可扩展性,使得代码更容易维护和扩展。
多态的前提条件有三个:
- 子类继承父类
- 子类重写父类的方法
- 父类引用指向子类的对象
详解Java this与super关键字的用法与区别
this 关键字有很多种用法,其中最常用的一个是,它可以作为引用变量,指向当前对象。
除此之外, this 关键字还可以完成以下工作。
- 调用当前类的方法;
this()
可以调用当前类的构造方法;- this 可以作为参数在方法中传递;
- this 可以作为参数在构造方法中传递;
- this 可以作为方法的返回值,返回当前类的对象。
super 关键字
“super 关键字的用法主要有三种。”
- 指向父类对象;
- 调用父类的方法;
super()
可以调用父类的构造方法。
在默认情况下,super()
是可以省略的,编译器会主动去调用父类的构造方法。也就是说,子类即使不使用 super()
主动调用父类的构造方法,父类的构造方法仍然会先执行。
super()
也可以用来调用父类的有参构造方法,这样可以提高代码的可重用性。