第一章:类和对象
一、面向对象编程
(一)面向对象编程的概念
- 万物皆对象。
- 面向对象指以属性和行为的观点去分析现实生活中的事物。
- 面向对象编程指先以面向对象的思想进行分析,然后使用面向对象的编程语言进行表达的过程。
- 面向对象编程是软件产业化发展的需求。
- 理解面向对象的思想精髓(封装、继承、多态),至少掌握一种编程语言。
二、类和对象及引用(重点)
(一)类和对象的概念
- 对象主要指现实生活中客观存在的实体,在Java语言中对象体现为内存空间中的一块存储区域(对象在堆区)。
- 类简单来就是“分类”,是对具有相同特征和行为的多个对象共性的抽象描述,在Java语言中体现为一种引用数据类型,里面包含了描述特征/属性的成员变量以及描述行为的成员方法。
- 类是用于构建对象的模板,对象的数据结构由定义它的类来决定。
(二)类的定义
class 类名 {
类体;
}
注意:通常情况下,当类名由多个单词组成时,要求每个单词首字母都要大写。
(三)成员变量的定义
class 类名 {
数据类型成员变量名= 初始值;
}
注意:当成员变量由多个单词组成时,通常要求从第二个单词起每个单词的首字母大写。
(四)对象的创建
new 类名();
注意:
a.当一个类定义完毕后,可以使用new关键字来创建该类的对象,这个过程叫做类的实例化。
b.创建对象的本质就是在内存空间的堆区申请一块存储区域,用于存放该对象独有特征信息。
(五)引用的定义
-
基本概念
a.使用引用数据类型定义的变量叫做引用型变量,简称为"引用"。
b.引用变量主要用于记录对象在堆区中的内存地址信息,便于下次访问。
-
语法格式
类名引用变量名;
引用变量名.成员变量名;
(六)成员变量的初始值
对象创建后,其成员变量可以按照默认的方式初始化,具体规则如下:
(七)类代码的执行流程和内存分析
public class Person {
String name;
int age;
public static void main(String[] args) {
// 1.声明Person类型的引用指向Person类
Person p = new Person();
// 2.打印对象中的成员变量值(默认值)
System.out.println(p.name + "今年" + p.age + "岁了");
System.out.println("------------------------------");
// 3.修改成员变量的数值
p.name = "zhangfei";
p.age = 30;
// 4.打印修改后的数值
System.out.println(p.name + "今年" + p.age + "岁了");
}
}
分析:
当类代码被加载进内存中时,首先代码会被加载在方法区中,运行时main方法为程序入口,所以在栈区中为main方法申请一块区域,然后类中定义的局部变量又在main方法的区域中分配出存储空间,new的对象将在堆区中申请一块存储空间,创建出的对象包含特征,未初始化赋值时,特征为默认值,赋值后将替换原先的默认值。
三、成员方法(重点)
(一)成员方法的定义
class 类名 {
返回值类型成员方法名(形参列表) {
成员方法体;
}
}
注意:
当成员方法名由多个单词组成时,要求从第二个单词起每个单词的首字母大写。
(二)返回值类型的详解
- 返回值主要指从方法体内返回到方法体外的数据内容。
- 返回值类型主要指返回值的数据类型,可以是基本数据类型,也可以是引用数据类型。
- 当返回的数据内容是66时,则返回值类型写int即可
- 在方法体中使用return关键字可以返回具体的数据内容并结束当前方法。
- 当返回的数据内容是66时,则方法体中写return 66; 即可
- 当该方法不需要返回任何数据内容时,则返回值类型写void即可。
(三)形参列表的详解
- 形式参数主要用于将方法体外的数据内容带入到方法体内部。
- 形式参数列表主要指多个形式参数组成的列表,语法格式如下:
数据类型形参变量名1, 数据类型形参变量名2, … - 当带入的数据内容是"hello"时,则形参列表写String s 即可
- 当带入的数据内容是66和"hello"时,则形参列表写inti, String s 即可
- 若该方法不需要带入任何数据内容时,则形参列表位置啥也不写即可。
(四)方法体的详解
- 成员方法体主要用于编写描述该方法功能的语句块。
- 成员方法可以实现代码的重用,简化代码(将重复的代码编写进成员方法中进行重复调用)。
(五)方法的调用
- 引用变量名.成员方法名(实参列表);
- 实际参数列表主要用于对形式参数列表进行初始化操作,因此参数的个数、类型以及顺序都要完全一致。
- 实际参数可以传递直接量、变量、表达式、方法的调用等。
(六)可变长参数
- 返回值类型方法名(参数的类型… 参数名)
- 方法参数部分指定类型的参数个数是可以改变的,也就是0~n个(看成一维数组使用即可)。
- 一个方法的形参列表中最多只能声明一个可变长形参,并且需要放到参数列表的末尾。
(七)方法法的传参过程
例:int max(int ia, int ib) { … … … }
int a = 5;
int b = 6;
int res = m.max(a,b);
- 为main方法中的变量a、b、res分配空间并初始化。
- 调用max方法,为max方法的形参变量ia、ib分配空间。
- 将实参变量的数值赋值到形参变量的内存空间中。
- max方法运行完毕后返回,形参变量空间释放。
- main方法中的res变量得到max方法的返回值。
- main方法结束后释放相关变量的内存空间。
(八)参数传递的注意事项(重点)
- 基本数据类型的变量作为方法的参数传递时,形参变量数值的改变通常不会影响到实参变量的数值,因为两个变量有各自独立的内存空间;
- 引用数据类型的变量作为方法的参数传递时,形参变量指向内容的改变会影响到实参变量指向内容的数值,因为两个变量指向同一块内存空间
- 当引用数据类型的变量作为方法的参数传递时,若形参变量 改变指向后 再改变指定的内容,则通常不会影响到实参变量指向内容的改变,因为两个变量指向不同的内存空间。
(九)内存结构之栈区
- 栈用于存放程序运行过程当中所有的局部变量。一个运行的Java程序从开始到结束会有多次方法的调用。
- JVM会为每一个方法的调用在栈中分配一个对应的空间,这个空间称为该方法的栈帧。一个栈帧对应一个正在调用中的方法,栈帧中存储了该方法的参数、局部变量等数据。
- 当某一个方法调用完成后,其对应的栈帧将被清除。
(十)传参的相关概念
- 参数分为形参和实参,定义方法时的参数叫形参,调用方法时传递的参数叫实参。
- 调用方法时采用值传递把实参传递给形参,方法内部其实是在使用形参。
- 所谓值传递就是当参数是基本类型时,传递参数的值,比如传递i=10,真实传参时,把10赋值给了形参。当参数是对象时,传递的是对象的值,也就是把对象的地址赋值给形参。
第二章:方法和封装
一、构造方法(重点)
(一)构造方法的基本概念
class 类名 {
类名(形参列表) {
构造方法体;
}
}
注意:
构造方法名与类名完全相同并且没有返回值类型,连void都不许有。
(二)默认构造方法
- 当一个类中没有定义任何构造方法时,编译器会自动添加一个无参空构造构造方法,叫做默认/缺省构造方法,如:Person(){}
- 若类中出现了构造方法,则编译器不再提供任何形式的构造方法。
(三)构造方法的作用
使用new关键字创建对象时会自动调用构造方法实现成员变量初始化工作。
二、方法重载
(一)概念
若方法名称相同,参数列表不同,这样的方法之间构成重载关系(Overload)。
(二)重载的体现形式
- 方法重载的主要形式体现在:参数的个数不同、参数的类型不同、参数的顺序不同,与返回值类型和形参变量名无关,但建议返回值类型最好相同。
- 判断方法能否构成重载的核心:调用方法时能否加以区分。
(三)重载的实际意义
- 方法重载的实际意义在于调用者只需要记住一个方法名就可以调用各种不同的版本,来实现各种不同的功能。
- 如:java.io.PrintStream类中的println方法。
三、this关键字(重点)
(一)基本概念
- 若在构造方法中出现了this关键字,则代表当前正在构造的对象。
- 若在成员方法中出现了this关键字,则代表当前正在调用的对象。
- this关键字本质上就是当前类类型的引用变量。
(二)工作原理
在构造方法中和成员方法中访问成员变量时,编译器会加上this.的前缀,而this.相当于汉语中"我的",当不同的对象调用同一个方法时,由于调用方法的对象不同导致this关键字不同,从而this.方式访问的结果也就随之不同。
(三)使用方式
- 当局部变量名与成员变量名相同时,在方法体中会优先使用局部变量(就近原则),若希望使用成员变量,则需要在成员变量的前面加上this.的前缀,明确要求该变量是成员变量。
- this关键字除了可以通过this.的方式调用成员变量和成员方法外,还可以作为方法的返回值。
- 在构造方法的第一行可以使用this()的方式来调用本类中的其它构造方法。
(四)注意事项
- 引用类型变量用于存放对象的地址,可以给引用类型赋值为null,表示不指向任何对象。
- 当某个引用类型变量为null时无法对对象实施访问(因为它没有指向任何对象)。此时,如果通过引用访问成员变量或调用方法,会产生NullPointerException异常。
四、方法递归调用
(一)递归的基本概念
递归本质就是指在方法体的内部直接或间接调用当前方法自身的形式。
(二)注意事项
- 使用递归必须有递归的规律以及退出条件。
- 使用递归必须使得问题简单化而不是复杂化。
- 若递归影响到程序的执行性能,则使用递推取代之。
例:编写成员方法实现费式数列中第n项的数值并返回
费式数列:1 1 2 3 5 8 13 21 ……
// 方法一:递归
int count(int n) {
// 当n=1或者n=2时,结果是1
if(1 == n || 2 == n) {
return 1;
}
// 否则结果是前两项的和
return count(n-1) + count(n-2);
}
// 方法二:递推
int count(int n) {
int ia = 1;
int ib = 1;
for(int i = 3; i <= n; i++) {
int ic = ia + ib;
ia = ib;
ib = ic;
}
return ib;
}
五、封装
(一)概念
- 通常情况下可以在测试类给成员变量赋值一些合法但不合理的数值,无论是编译阶段还是运行阶段都不会报错或者给出提示,此时与现实生活不符。
- 为了避免上述错误的发生,就需要对成员变量进行密封包装处理,来隐藏成员变量的细节以及保证成员变量数值的合理性,该机制就叫做封装。
(二)封装的实现流程
- 私有化成员变量,使用private关键字修饰。
- 提供公有的get和set方法,并在方法体中进行合理值的判断。
- 在构造方法中调用set方法进行合理值的判断。
(三)JavaBean的概念
- JavaBean是一种Java语言写成的可重用组件,其它Java 类可以通过反射机制发现和操作这些JavaBean 的属性。
- JavaBean本质上就是符合以下标准的Java类:
1)类是公共的
2)有一个无参的公共的构造器
3)有属性,且有对应的get、set方法
第三章:static关键字和继承
一、static关键字(重点)
(一)基本概念
- 使用static关键字修饰成员变量表示静态的含义,此时成员变量由对象层级提升为类层级,也就是整个类只有一份并被所有对象共享,该成员变量随着类的加载准备就绪,与是否创建对象无关。
- static关键字修饰的成员可以使用引用.的方式访问,但推荐类名.的方式。静态成员方法中没有this关键字,因为是可以通过类名.方式调用的。
(二)使用方式
- 在非静态成员方法中既能访问非静态的成员又能访问静态的成员。
(成员:成员变量+ 成员方法,静态成员被所有对象共享) - 在静态成员方法中只能访问静态成员不能访问非静态成员。
(成员:成员变量+ 成员方法,因为此时可能还没有创建对象) - 在以后的开发中只有隶属于类层级并被所有对象共享的内容才可以使用static关键字修饰。(不能滥用static关键字)
(三)构造块和静态代码块(重点)
- 构造块:在类体中直接使用{ }括起来的代码块。
- 每创建一个对象都会执行一次构造块。
- 静态代码块:使用static关键字修饰的构造块。
- 静态代码块随着类加载时执行一次。
拓展:
构造块执行于构造方法体之前,通常是用来编写准备工作的相关代码,如:对成员变量进行统一初始化操作;而静态代码块是随着类的加载就准备就绪的,所以静态代码块会先与构造块之前执行,主要是用于做一些需要随着类的加载就要做的准备工作,如:数据库的驱动包等。
注意:
- 先执行父类的静态代码块,再执行子类的静态代码块。
- 然后执行父类的构造块,父类的构造方法体。
- 最后才执行子类的构造块,子类的构造方法体。
public class SuperTest {
{
System.out.println("SuperTest类中的构造块!"); // (2) c
}
static {
System.out.println("SuperTest类中的静态代码块!"); // (1) a
}
public SuperTest() {
System.out.println("SuperTest类中的构造方法体!"); // (3) d
}
public static void main(String[] args) {
// 使用无参方式构造对象,同一个类中执行顺序为:(1)(2)(3)
SuperTest st = new SuperTest();
}
}
public class SubSuperTest extends SuperTest {
{
System.out.println("==========SubSuperTest类中的构造块!"); // e
}
static {
System.out.println("==========SubSuperTest类中的静态代码块!"); // b
}
public SubSuperTest() {
System.out.println("==========SubSuperTest类中的构造方法体!"); // f
}
public static void main(String[] args) {
// 父类引用指向子类类型,形成多态,此时执行的顺序为abcdef
SuperTest st = new SubSuperTest();
}
}
(四)单例设计模式(重点)
1)概念
在某些特殊场合中,一个类对外提供且只提供一个对象时,这样的类叫做单例类,而设计单例的流程和思想叫做单例设计模式。主要是为了节省内存资源、保证数据内容的一致性;一般常用在工具类的实现或创建对象需要消耗资源。例如:Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。
2)实现流程
- 声明本类类型的引用指向本类类型的对象,并使用private static关键字共同修饰。(定义一个静态私有实例)
- 私有化构造方法,使用private关键字修饰。(将类的构造函数设为私有,外部类就无法调用该构造函数,也就无法生成多个实例。)
- 提供公有的get方法负责将对象返回出去,并使用public static关键字共同修饰。(向外提供一个静态的公有函数用于创建或获取该静态私有实例。)
3)实现方式
单例设计模式的实现方式有两种:饿汉式和懒汉式,在以后的开发中推荐饿汉式。
1、饿汉式
/**
* 单例模式(饿汉式)
* 线程安全,因为类一加载就进行了初始化,但也因此容易产生垃圾,损耗性能
*/
public class SingletonHungry {
// 1.声明本类类型的引用指向本类类型的对象,使用private static关键字共同修饰
private static SingletonHungry sh = new SingletonHungry();
// 2.私有化构造方法,使用private关键字修饰
private SingletonHungry() {
}
// 3.提供公有的get方法负责将对象返回出去,使用public static关键字共同修饰
public static SingletonHungry getInstance() {
return sh;
}
}
2、简单线程非安全的懒汉式
/**
* 单例模式(简单线程非安全的懒汉式)
* 线程不安全,多线程高并发情况下可能会导致创建多个实例,
* 延迟初始化,类加载时没有生成单例,只有调用 getlnstance() 方法时才去创建这个单例
*/
public class SingletonLazy {
private static SingletonLazy sl;
private SingletonLazy() {
}
public static SingletonLazy getInstance() {
if (null == sl) { // (1)
sl = new SingletonLazy(); // (2)
}
return sl;
}
}
线程不安全的原因:
在多线程的情况下,会存在以下假设的情景:
假设两个线程并发调用了getInstance(),若线程一先到代码中的步骤(1)进行了判断,进入到步骤(2)但还没开始执行时,JVM将CPU资源切换给了线程二,由于线程一还没有执行步骤(2),那么sl还是空的,因此线程二同样进入了步骤(2)并实现了实例化的操作,当线程一重新被唤醒后,同样又进行了一次实例化,这便会产生了多个实例,所以在多线程下不可以直接使用这种方式。
3、同步锁模式下的懒汉式
/**
* 单例模式(同步锁模式下的懒汉式)
* 线程安全,但性能较低
*/
public class SingletonSynLock {
private static SingletonSynLock ssl;
private SingletonSynLock() {
}
public synchronized static SingletonSynLock getInstance() {
if (null == ssl) {
ssl = new SingletonSynLock();
}
return ssl;
}
}
同步锁:
给getInstance()方法添加synchronized关键字,也就是给整个方法添加同步锁,这样一来,在多线程的情况下,当一个线程进入这个方法后,其他线程都将被互斥在方法外,必须等这个线程离开这个方法后,第二个线程才能进入,这便可以解决多线程并发访问的问题,但这种方式会产生一个问题,那就是当第一个进入的线程创建好对象并实例化离开后,后面的线程仍要通过同步互斥来获取对象,此过程完全可以通过并发来执行的,我们只需要同步实例化那一步(ssl = new SingletonSynLock();)即可,而现在相当于按序执行,所以性能有所损耗。
4、双重锁模式的懒汉式
/**
* 单例模式(双重锁模式的懒汉式)
* 线程安全,延迟初始化
* 这种方式可以确保在多线程的模式下安全且保持较高的性能
*/
public class SingletonDoubleLock {
private volatile static SingletonDoubleLock sdl; // (1)
private SingletonDoubleLock() {
}
public static SingletonDoubleLock getInstance() {
if (null == sdl) {
synchronized (SingletonDoubleLock.class) {
if (null == sdl) {
sdl = new SingletonDoubleLock(); // (2)
}
}
}
return sdl;
}
}
双重锁:
首先我们要先大概了解下java的内存模型(JMM),简单来说就是共享变量一开始都存放在主内存中,而每个线程也都有一个自己的高速缓冲内存,线程进行读写操作之前,先将主内存中的共享变量拷贝到自己的高速缓冲内存中,然后在高速缓冲内存中进行各种操作,之后再将完成操作后的共享变量重新写入主内存中。这种方式在单线程中是没有问题的,但在多线程下就会产生缓存一致性的问题,比如:i++的操作,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程一进行加1操作后把最新值1写入到主内存。而此时线程二的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程二再把i的值写入主内存。最终结果i的值是1,而不是2。
在代码(1)添加volatile关键字的原由,一方面保证不同线程对这个volatile变量进行读取时的可见性,即一个线程在高速缓存中修改volatile变量后,JMM会把该变量强制刷新到主内存中去,而且这个写入操作会导致其他线程中的volatile变量缓存无效,以致于其他线程必须重新从主内存中获取变量,这样一来,一个线程修改了某个变量的值后,这新值对其他线程来说是立即可见的。另一方面可以禁止进行指令重排序,阻止编译器对代码的优化。所谓重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段,而重排序操作不会对存在数据依赖关系的操作进行重排序,如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序;重排序主要是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变,如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。 重排序在单线程下一定能保证结果的正确性,但是在多线程环境下则可能因为发生重排序从而影响结果,就如代码中的sdl = new SingletonDoubleLock();该行代码可以拆分为三行的伪代码,即
①分配对象的内存空间:memory = allocate();
②初始化对象:ctorInstance(memory);
③设置sdl变量指向刚分配的内存地址:sdl = memory。
由于②③之间没有依赖关系,所以可能发生重排序,造成②还没有初始化完成,③中的变量就拿到了值,从而出现异常问题。回到上面单例模式的代码中,若一个线程在步骤(2)发生了指令重排序,按照①③②顺序执行,在执行完③还没有执行到②时,sdl变量已经拿到值,此时由于synchronized锁的可见性,其他线程会去主内存重新读取sdl的值,并且返回了有值的sdl,但是此时实际上SingletonDoubleLock类并没有完成实例化,SingletonDoubleLock对象也并没有生成,所以此时返回的是一个未初始化的对象。所以当声明对象的引用为volatile后,三行伪代码中的②和③之间的重排序,在多线程环境中将会被禁止,类在实例化过程中会严格按照①②③顺序执行下去。
虽然volatile关键字保证了代码的可见性和一定程度上的有序性,但无法保证原子性。所谓的原子性是指:一个操作中cpu不可以在中途暂停然后再调度,既不会被中断操作,要不直接一次全部执行完成,要不就不执行。如:int a = 10;这种就为原子性操作,而i++操作这种是线程先从主内存中读i的值,然后copy到工作内存中,在工作内存进行修改i的值+1;最后把i的最新值写入主内存中,所以i++操作是非原子性操作。由于sdl = new SingletonDoubleLock();可以拆分三行的伪代码,此操作也为非原子性操作,虽然volatile可以禁止指令重排序,却无法保证在执行步骤的过程由于系统调度问题而被中断的问题,如当一个线程执行完①②伪代码后,CPU切换资源给其他线程,该线程的执行步骤被中断,此时sdl仍然为空,所以其他线程同样还可以创建实例,那么当第一个线程被重新唤醒后,接着执行完③步骤后又创建了实例,这样一来就创建了多个实例,所以为了防止上面这个问题,添加synchronized来给进行实例化的代码块进行同步锁从而保证原子性,保证只有一个线程可以进入并进行实例化,其他线程都将被同步互斥在外。
5、静态内部类单例模式
/**
* 静态内部类单例模式
* 不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化,还有保持了高性能
*/
public class SingletonStatic {
private SingletonStatic() {
}
private static class Inner {
private static SingletonStatic instance = new SingletonStatic();
}
public static SingletonStatic getInstance() {
return Inner.instance;
}
}
当外部类加载时并没有立即加载内部类,内部类不被加载则不去初始化instance,故而不占内存。只有当getInstance()方法第一次被调用时,才会去初始化instance,第一次调用getInstance()方法会导致虚拟机加载Inner类,而getInstance()方法返回的是Inner类中的被static修饰的instance对象,与上面几个模式不同的是,getInstance()方法并没有多次去new对象,所以不管多少个线程去调用getInstance()方法,取的都是同一个instance对象,而不用去重新创建,也就是说只有一个线程可以获得对象的初始化,其他线程无法再进行初始化,保证对象的唯一性。目前此方式是所有单例模式中最推荐的模式,但具体还是根据项目选择。静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去。
6、枚举类单例
/**
* 枚举单例
*/
public enum SingletonEnum {
INSTANCE; // INSTANCE 就是SingletonEnum的常量,只会初始化一次,天生为单例
public void doYouWantToDo() {
System.out.println("doYouWantToDo");
}
}
枚举实现单例,代码简洁不易出错,无须像饿汉式一样直接在类加载时初始化,也无须像懒汉式一样需要双重检查锁定,直接由JVM保证线程安全,不会因为序列化生成新实例,也不会因为反射生成新实例。
注:单例模式是创建型模式,都会新建一个实例,那么会有个重要的问题就是反序列化,也就是当实例被写入文件,再从文件反序列化为实例,为了确保实例的唯一性,需要在单例重写readResolve方法(枚举单例无需重写也能保证唯一性),如:
private Object readResolve() {
return instance;
}
二、继承(重点)
(一)概念
- 当多个类之间有相同的特征和行为时,可以将相同的内容提取出来组成一个公共类,让多个类吸收公共类中已有特征和行为而在多个类型只需要编写自己独有特征和行为的机制,叫做继承。
- 在Java语言中使用extends(扩展)关键字来表示继承关系。
- 如:
public class Worker extends Person{} -表示Worker类继承自Person类
其中Person类叫做超类、父类、基类。
其中Worker类叫做派生类、子类、孩子类。 - 使用继承提高了代码的复用性,可维护性及扩展性,是多态的前提条件。
(二)特点
- 子类不能继承父类的构造方法和私有方法,但私有成员变量可以被继承只是不能直接访问。
- 无论使用何种方式构造子类的对象时都会自动调用父类的无参构造方法,来初始化从父类中继承的成员变量,相当于在构造方法的第一行增加代码super()的效果。
- 使用继承必须满足逻辑关系:子类is a 父类,也就是不能滥用继承。
- Java语言中只支持单继承不支持多继承,也就是说一个子类只能有一个父类,但一个父类可以有多个子类。
/**
* 父类
*/
public class Person {
private String name;
private int age;
public Person() {
System.out.println("Person无参构造");
}
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Person有参构造");
}
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 class Worker extends Person {
private int salary;
public Worker() {
System.out.println("Worker无参构造");
}
public Worker(String name, int age, int salary) {
super(name, age);
this.salary = salary;
System.out.println("Worker有参构造");
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
}
public class WorkerTest {
public static void main(String[] args) {
// 1.使用无参方式构造Worker类型的对象
Worker w1 = new Worker();
// 此时打印内容和顺序为:Person无参构造 -> Worker无参构造
// 2.使用有参方式构造Worker类型的对象
Worker w2 = new Worker("abc",20,5000);
// 此时打印内容和顺序为:Person有参构造 -> Worker有参构造
}
}
从上面代码可以看出,子类在创建自身对象时父类的对象其实也被包含其中,所以为了构造子类类型的对象就必须先构造父类类型的对象,而构造父类的对象时则一定会调用父类类型的构造方法,所以创建子类类型对象时,会先执行父类的构造方法,再执行子类的构造方法。
(三)方法重写
1)概念
从父类中继承下来的方法不满足子类的需求时,就需要在子类中重新写一个和父类一样的方法来覆盖从父类中继承下来的版本,该方式就叫做方法的重写(Override)。
2)原则
- 要求方法名相同、参数列表相同以及返回值类型相同,从Java5开始允许返回子类类型。
- 要求方法的访问权限不能变小,可以相同或者变大。
- 要求方法不能抛出更大的异常(异常机制)。
三、访问控制
(一)常用的访问控制符
(二)注意事项
- public修饰的成员可以在任意位置使用。
- private修饰的成员只能在本类内部使用。
- 通常情况下,成员方法都使用public关键字修饰,成员变量都使用private关键字修饰。
(三)package语句的由来
- 定义类时需要指定类的名称,但如果仅仅将类名作为类的唯一标识,则不可避免的出现命名冲突的问题。这会给组件复用以及团队间的合作造成很大的麻烦!
- 在Java语言中,用包(package)的概念来解决命名冲突的问题。
1)包的定义
- 在定义一个类时,除了定义类的名称一般还要指定一个包名,格式如下:
package 包名;
package 包名1.包名2.包名3…包名n; - 为了实现项目管理、解决命名冲突以及权限控制的效果。
2)定义包的规范
- 如果各个公司或开发组织的程序员都随心所欲的命名包名的话,仍然不能从根本上解决命名冲突的问题。因此,在指定包名的时候应该按照一定的规范。
org.apache.commons.lang.StringUtil - 其中StringUtils是类名而org.apache.commons.lang是多层包名,其含义如下:org.apache表示公司或组织的信息(是这个公司(或组织)域名的反写);common 表示项目的名称信息;lang 表示模块的名称信息。
3)包的导入
•使用import关键字导入包。
•使用import关键字导入静态成员,从Java5.0开始支持。
四、final关键字(重点)
(一)基本概念
final本意为"最终的、不可改变的",可以修饰类、成员方法以及成员变量。
(二)使用方式
- final关键字修饰类体现在该类不能被继承。
-主要用于防止滥用继承,如:java.lang.String类等。 - final关键字修饰成员方法体现在该方法不能被重写但可以被继承。
-主要用于防止不经意间造成重写,如:java.text.Dateformat类中format方法等。 - final关键字修饰成员变量体现在该变量必须初始化且不能改变。
-主要用于防止不经意间造成改变,如:java.lang.Thread类中MAX_PRIORITY等。
(三)常量的概念
- 在以后的开发中很少单独使用final关键字来修饰成员变量,通常使用public static final关键字共同修饰成员变量来表达常量的含义,常量的命名规范要求是所有字母都要大写,不同的单词之间采用下划线连。
- public static final double PI = 3.14;
第四章:多态和特殊类
一、多态(重点)
(一)概念
多态主要指同一种事物表现出来的多种形态。
(二)语法格式
父类类型引用变量名= new 子类类型();
如:Shape sr= new Rect(); sr.show();
(三)多态的特点
- 当父类类型的引用指向子类类型的对象时,父类类型的引用可以直接调用父类独有的方法。
- 当父类类型的引用指向子类类型的对象时,父类类型的引用不可以直接调用子类独有的方法。
- 对于父子类都有的非静态方法来说,编译阶段调用父类版本,运行阶段调用子类重写的版本(动态绑定)。
- 对于父子类都有的静态方法来说,编译和运行阶段都调用父类版本。
(四)引用数据类型之间的转换
- 引用数据类型之间的转换方式有两种:自动类型转换和强制类型转换。
- 自动类型转换主要指小类型向大类型的转换,也就是子类转为父类,也叫做向上转型。
- 强制类型转换主要指大类型向小类型的转换,也就是父类转为子类,也叫做向下转型或显式类型转换。
- 引用数据类型之间的转换必须发生在父子类之间,否则编译报错。
- 若强转的目标类型并不是该引用真正指向的数据类型时则编译通过,运行阶段发生类型转换异常(ClassCastException)。
- 为了避免上述错误的发生,应该在强转之前进行判断,格式如下:
if(引用变量 instanceof 数据类型)
判断引用变量指向的对象是否为后面的数据类型
public class Shape {
private int x;
private int y;
public Shape() {
}
public Shape(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public void show() {
System.out.println("横坐标:" + getX() + ",纵坐标:" + getY());
}
// 自定义静态方法
public static void test() {
System.out.println("Shape类中的静态方法!");
}
}
public class Rect extends Shape {
private int len;
private int wid;
public Rect() {
}
public Rect(int x, int y, int len, int wid) {
super(x, y);
this.len = len;
this.wid = wid;
}
public int getLen() {
return len;
}
public void setLen(int len) {
this.len = len;
}
public int getWid() {
return wid;
}
public void setWid(int wid) {
this.wid = wid;
}
@Override
public void show() {
super.show();
System.out.println("长度是:" + getLen() + ",宽度是:" + getWid());
}
public void onlyRect() {
System.out.println("Rect类独有的方法");
}
// @Override Error: 不是真正意义上的重写,重写是动态的
// 如果一个static方法被调用了,JVM不会检查什么类型正在指向它,它只会调用跟父类相关联的方法
// 非staic方法被调用时,JVM会通过句柄检查正在指向的类的类型,并调用此类型相关的方法,从而实现了重写
public static void test() {
System.out.println("Rect类中的静态方法!");
}
}
public class Circle extends Shape {
private int ir;
public Circle() {
}
public Circle(int x, int y, int ir) {
super(x, y);
this.ir = ir;
}
public int getIr() {
return ir;
}
public void setIr(int ir) {
this.ir = ir;
}
@Override
public void show() {
super.show();
System.out.println("半径是:" + getIr());
}
}
public class ShapeTest {
public static void main(String[] args) {
// 1.声明Shape类型的引用指向Shape类型的对象并打印特征
Shape s1 = new Shape(1, 2);
// 当Rect类中没有重写show方法时,下面调用Shape类中的show方法
// 当Rect类中重写show方法后,下面也只能调用Shape类中的show方法
s1.show(); // 1 2
System.out.println("------------------------------------");
// 2.声明Rect类型的引用指向Rect类型的对象并打印特征
Rect r1 = new Rect(3, 4, 5, 6);
// 当Rect类中没有重写show方法时,下面调用Shape类中的show方法
// 当Rect类中重写show方法后,下面调用Rect类中的show方法
r1.show(); // 3 4 5 6
System.out.println("------------------------------------");
// 3.声明Shape类型的引用指向Rect类型的对象并打印特征,形成多态(多态使用场合一)
// 相当于从Rect类型到Shape类型的转换,也就是子类向父类转换,小到大的转换,自动类型转换,向上转型
Shape sr = new Rect(7, 8, 9, 10);
// 当Rect类中没有重写show方法时,下面调用Shape类中的show方法
// 当Rect类中重写show方法后,下面的代码在编译阶段调用Shape类的方法,在运行阶段调用Rect类中的show方法
sr.show(); // 7 8 9 10
System.out.println("------------------------------------");
// 4.测试Shape类型的引用能否直接调用父类和子类独有的方法
int ia = sr.getX();
System.out.println("获取到的横坐标是:" + ia); // 7
// sr.onlyRect(); error Shape类中找不到onlyRect方法,也就是不能直接调用子类中独有的方法
System.out.println("------------------------------------");
// 5.通过强制性转换来使用父类类型的引用调用子类独有方法的方式
// 相当于从Shape类型到Rect类型的转换,也就是父类到子类的转换,大到小的转换,强制类型转换,向下转型
((Rect) sr).onlyRect();
// 希望将Shape类型转换为String类型
// String str = (String)sr; Error 强制类型转换要求必须拥有父子类关系
// 希望将Shape类型强制转换为Circle类型,下面没有报错
//Circle c1 = (Circle)sr; // 编译ok,但运行阶段发生 ClassCastException类型转换异常,所以强转的目标类型必须为引用真正指向的数据类型
System.out.println("------------------------------------");
// 6.为了避免强转时发生的错误,在强制类型转换之前应该使用instanceof进行类型的判断
// 判断sr指向堆区内存中的对象是否为Rect类型,若是则返回true,否则返回false
if (sr instanceof Rect) {
Rect rt = (Rect)sr;
sr.show();
} else {
System.out.println("强转需谨慎!");
}
System.out.println("------------------------------------");
// 7.调用静态方法
// 对于父子类都有的静态方法来说,子类不会覆盖父类,编译和运行阶段都将调用父类的静态方法
sr.test(); // 提示:不建议使用引用.的方式访问
Shape.test(); // 推荐使用类名.的方式访问
System.out.println("------------------------------------");
// 8.通过参数传递形成了多态(多态使用场合二)
ShapeTest.draw(new Rect(1, 2, 3, 4));
ShapeTest.draw(new Circle(5, 6, 7));
}
public static void draw(Shape s) {
// 编译阶段调用父类的版本,运行阶段调用子类重写以后的版本
s.show();
}
}
(五)多态的实际意义
多态的实际意义在于屏蔽不同子类的差异性实现通用的编程带来不同的效果。
二、抽象类(重点)
(一)概念
- 抽象方法主要指不能具体实现的方法并且使用abstract关键字修饰,也就是没有方法体。
- 具体格式如下:
访问权限 abstract 返回值类型方法名(形参列表);
如:public abstract void cry(); - 抽象类主要指不能具体实例化的类并且使用abstract关键字修饰,也就是不能创建对象。
(二)抽象类和抽象方法的关系
- 抽象类中跟普通类一样,可以有成员变量、构造方法、成员方法;
- 抽象类中可以没有抽象方法,也可以有抽象方法;
- 拥有抽象方法的类必须是抽象类,因此真正意义上的抽象类应该是具有抽象方法并且使用abstract关键字修饰的类。
(三)抽象类的实际意义
- 抽象类的实际意义不在于创建对象而在于被继承。
- 当一个类继承抽象类后必须重写抽象方法,否则该类也变成抽象类,也就是抽象类对子类具有强制性和规范性,因此叫做模板设计模式。
建议:
- 在以后的开发中推荐使用多态的格式,此时父类类型引用直接调用的所有方法一定是父类中拥有的方法,若以后更换子类时,只需要将new关键字后面的子类类型修改而其它地方无需改变就可以立即生效,从而提高了代码的可维护性和可扩展型。
- 该方式的缺点就是:父类引用不能直接调用子类独有的方法,若调用则需要强制类型转换。
/**
* 抽象类
*/
public abstract class AbstractTest {
private String str;
public AbstractTest() {
}
public AbstractTest(String str) {
this.str = str;
}
public String getStr() {
return str;
}
public void setStr(String str) {
this.str = str;
}
// 抽象类中可以有非抽象方法
public void unAbstractWay() {
System.out.println("抽象类AbstractTest中的非抽象方法");
}
// 抽象类中可以有抽象方法(没有方法体),没有也是可以的
public abstract void abstractWay();
// private 和 abstract 关键字不能共同修饰一个方法,抽象方法需要被重写,private修饰只允许在本来中使用,矛盾
//private abstract void abstractWay();
// final 和 abstract 关键字不能共同修饰一个方法,final修饰的方法不能被重写,矛盾
//public final abstract void abstractWay();
// static 和 abstract 关键字不能共同修饰一个方法
// static修饰的方法为静态方法,可以被类直接调用,而abstract修饰的抽象方法没有方法体,不能够被直接调用,矛盾
//public static abstract void abstractWay();
public static void main(String[] args) {
// 声明本类类型的引用指向本类类型的对象
//AbstractTest at = new AbstractTest(); // 编译报错,Abstract为抽象类,不能实例化,
// 需声明为匿名类并重写抽象方法才可以
AbstractTest at = new AbstractTest() {
@Override
public void abstractWay() {
System.out.println("匿名内部类重写抽象类型AbstractTest中的抽象方法");
}
}; // 注意此处要有分号
at.unAbstractWay(); // 调用非抽象方法
at.abstractWay(); // 调用重写后的方法
System.out.println(at.str); // 成员变量的值,初始化的默认值为null
}
}
/**
* 继承抽象类AbstractTest的子类需同为抽象类或者重写AbstractTest类中抽象方法
*/
public class SubAbstractTest extends AbstractTest {
@Override
public void abstractWay() {
System.out.println("子类SubAbstractTest中重写的abstractWay方法");
}
// SubAbstractTest自身独有的方法
public void show() {
System.out.println("SubAbstractTest自身独有的方法");
}
public static void main(String[] args) {
// 1.声明本类类型的引用指向本类类型的对象,没有形成多态
SubAbstractTest sat = new SubAbstractTest();
sat.abstractWay(); // 调用重写后的方法
sat.show(); // 调用自身独有的方法
System.out.println(sat.getStr()); // 通过公共方法获取父类声明的成员变量 初始 null
System.out.println("-------------------------------");
// 2.声明AbstractTest类型的引用指向子类的对象,形成多态
// 多态的使用场合之一: 直接在方法体中使用抽象类的引用指向子类类型的对象
AbstractTest at = new SubAbstractTest();
at.abstractWay(); // 编译阶段调用父类版本,运行阶段调用子类版本
// 通过强转调用子类独有的方法
((SubAbstractTest) at).show();
System.out.println(at.getStr()); // 需要通过公共方法才能获取父类声明的成员变量 初始 null
}
}
三、接口(重点)
(一)基本概念
- 接口就是一种比抽象类还抽象的类,体现在所有方法都为抽象方法。
- 定义类的关键字是class,而定义接口的关键字是interface。
(二)类和接口之间的关系(重点)
(三)抽象类和接口的主要区别(重点)
- 定义抽象类的关键字是abstract class,而定义接口的关键字是interface。
- 继承抽象类的关键字是extends,而实现接口的关键字是implements。
- 继承抽象类支持单继承,而实现接口支持多实现。
- 抽象类中可以有构造方法,而接口中不可以有构造方法。
- 抽象类中可以有成员变量,而接口中只可以有常量。
- 抽象类中可以有成员方法,而接口中只可以有抽象方法。
- 抽象类中增加方法时子类可以不用重写,而接口中增加方法时实现类需要重写(Java8以前的版本只可以有抽象方法)。
- 从Java8开始增加新特性,接口中允许出现非抽象方法和静态方法,但非抽象方法需要使用default关键字修饰。
- 从Java9开始增加新特性,接口中允许出现私有方法。
/**
* 接口类
*/
public interface InterfaceTest {
// 接口中只可以有常量
public static final int NUM = 1;
// public static final可以省略不写,接口默认直接定义就是常量
int NUM2 = 2;
// Java8以前的版本只可以有抽象方法
public abstract void test(); // public abstract可以省略,但建议写上,增加可读性
// Java8增加新特性,接口中允许出现非抽象方法和静态方法,但非抽象方法需要使用default关键字修饰
default void show() {
System.out.println("接口中的非抽象方法!");
}
static void show2() {
System.out.println("接口中的静态方法!");
}
// Java9增加新特性,接口中允许出现私有方法
// 允许私有方法的原因:
// 一方面可以提高代码的可重用性,无需编写重复代码,如一组非抽象方法中都重复的代码,那么就可以将这些重复代码抽取出来到一个私有方法中
// 另一方面我们可以选择只将我们想要的方法对外暴露
private void show3() {
System.out.println("接口中的私有方法!");
}
}
public class SubInterfaceTest implements InterfaceTest {
@Override
public void test() {
System.out.println("实现InterfaceTest接口的SubInterfaceTest类中重写的抽象方法");
}
public static void main(String[] args) {
// 通过接口名.常量名的方式调用接口中定义的常量,不能被修改
//InterfaceTest.NUM = 3;
//InterfaceTest.NUM2 = 3;
System.out.println(InterfaceTest.NUM);
// 创建本类对象
SubInterfaceTest sit = new SubInterfaceTest();
// 通过实例对象来调用接口中的抽象方法
sit.test();
// 还可以通过匿名内部类的方式在方法体中重写接口中的抽象方法,然后再调用抽象方法
new InterfaceTest() {
@Override
public void test() {
System.out.println("匿名内部类中重写接口的抽象方法");
}
}.test();
// 调用接口中定义的default方法(非抽象方法)只能通过实例对象来调用
sit.show();
// 调用接口中定义的静态方法,可通过接口名.方法名()的方式来调用
InterfaceTest.show2();
// 声明接口类型的引用指向实现类的对象,形成多态
InterfaceTest it = new SubInterfaceTest();
it.test();
it.show();
//it.show2(); //编译报错,调用静态方法,需通过接口名.方法名()的方式来调用
}
}
第四章:内部类
一、内部类(熟悉)
(一)基本概念
- 当一个类的定义出现在另外一个类的类体中时,那么这个类叫做内部类(Inner),而这个内部类所在的类叫做外部类(Outer)。
- 类中的内容:成员变量、成员方法、构造方法、静态成员、构造块和静态代码块、内部类。
- 编译后会生成一个命名为 外部类名$内部类名.class 的字节码
(二)实际作用
当一个类存在的价值仅仅是为某一个类单独服务时,那么就可以将这个类定义为所服务类中的内部类,这样可以隐藏该类的实现细节并且可以方便的访问外部类的私有成员而不再需要提供公有的get和set方法。
(三)内部类的分类
- 普通内部类-直接将一个类的定义放在另外一个类的类体中。
- 静态内部类-使用static关键字修饰的内部类,隶属于类层级。
- 局部内部类-直接将一个类的定义放在方法体的内部时。
- 匿名内部类-就是指没有名字的内部类。
(四)内部类的格式和使用方式
1)普通(成员)内部类的格式
访问修饰符 class 外部类的类名{
访问修饰符 class 内部类的类名{
内部类的类体;
}
}
2)普通内部类的使用方式
- 普通内部类和普通类一样可以定义成员变量、成员方法以及构造方法等。
- 普通内部类和普通类一样可以使用final或者abstract关键字修饰。
- 普通内部类还可以使用private或protected关键字进行修饰。
- 普通内部类需要使用外部类对象来创建对象。
- 如果内部类访问外部类中与本类内部同名的成员变量或方法时,需要使用外部类名.this.变量名(方法名)的方式。
/**
* 普通内部类的示例
*/
public class NormalOuter {
// 外部类定义的成员变量
private int ia = 1;
public void show() {
System.out.println("外部类中的show方法");
}
// 定义普通内部类,隶属于外部类的成员,并且是外部类的对象层级
public class NormalInner {
// 内部类中也可以定义成员变量
private int ia = 2; // 可以定义跟外部类同名的成员变量,但使用时要加以区分
private int ib = 3;
// 构造方法
public NormalInner() {
System.out.println("普通内部类中的构造方法");
}
// 成员方法
public void show() {
NormalOuter.this.show(); // 调用外部类同名的方法,this不可以省略
System.out.println("与外部类同名的成员变量不作为形参时,内部类中变量ia的数值为:" + this.ia); // this可以省略
System.out.println("与外部类同名的成员变量不作为形参时,外部类中变量ia的数值为:" + NormalOuter.this.ia);
}
public void show2(int ia) {
System.out.println("与外部类同名的成员变量作为形参时,直接取ia的值会遵守局部优先原则,ia的数值为:" + ia); // 形参的值
System.out.println("与外部类同名的成员变量作为形参时,内部类中变量ia的数值为:" + this.ia);
System.out.println("与外部类同名的成员变量作为形参时,外部类中变量ia的数值为:" + NormalOuter.this.ia);
}
}
}
public class NormalOuterTest {
public static void main(String[] args) {
// 1.声明NormalOuter类型的引用指向该类型的对象
NormalOuter no = new NormalOuter();
// 2.声明NormalOuter类中内部类的引用指向内部类的对象
// 内部类对象的创建依赖于外部类对象;内部类对象持有指向外部类对象的引用
NormalOuter.NormalInner ni = no.new NormalInner();
// 调用内部类中的方法
ni.show();
System.out.println("----------------------------------------------------");
ni.show2(4); // ia的值分别为:4 2 1
}
}
3)静态内部类的格式
访问修饰符 class 外部类的类名{
访问修饰符 static class 内部类的类名{
内部类的类体;
}
}
4)静态内部类的使用方式
- 静态内部类不能直接访问外部类的非静态成员。
- 静态内部类可以直接创建对象。
- 如果静态内部类访问外部类中与本类内同名的成员变量或方法时,需要使用类名.的方式访问。
/**
* 静态内部类的示例
*/
public class StaticOuter {
private int ia = 1; // 隶属于对象层级
private static int ib = 2; // 隶属于类层级
public void show() {
System.out.println("外部类的非静态方法show()");
}
public static void show2() {
System.out.println("外部类的静态方法show2()");
}
// 定义静态内部类,有static关键字修饰隶属于类层级
public static class StaticInner {
private static int ib = 3;
private int ic = 4;
public StaticInner() {
System.out.println("静态内部类的构造方法");
}
public void show() {
System.out.println("ic = " + ic); // 静态内部类中的成员变量 4
//System.out.println("外部类的ia为:" + ia); // 不能直接访问外部类的非静态成员变量,因为此时还没有创建对象
// 静态内部类中可以直接创建外部类对象,然后通过对象去访问外部类的非静态成员变量
StaticOuter so = new StaticOuter();
System.out.println("外部类的ia为:" + so.ia); // 1
// 访问外部类的静态成员变量
// System.out.println("外部类的ib为:" + ib); // 当内部类不存在同名的变量时,可以直接访问外部类的静态成员变量
System.out.println("外部类的ib为:" + StaticOuter.ib); // 当内部类存在同名的变量时,使用类名.的方式访问 2
}
public void show2(int ib) {
System.out.println("ib = " + ib); // 就近原则 形参 5
System.out.println("内部类中的静态变量ib = " + StaticInner.ib); // 3
System.out.println("外部类中的静态变量ib = " + StaticOuter.ib); // 2
// 静态内部类中访问外部类的静态方法
StaticOuter.show2();
// 静态内部类中访问外部类中与本类内同名的非静态方法
new StaticOuter().show();
}
}
}
/**
* 测试类
*/
public class StaticOuterTest {
public static void main(String[] args) {
// 声明StaticInner类型的引用指向该类型的对象
StaticOuter.StaticInner si = new StaticOuter.StaticInner();
// 调用内部类的方法
si.show();
si.show2(5);
}
}
5)局部(方法)内部类的格式
访问修饰符 class 外部类的类名{
访问修饰符 返回值类型 成员方法名(形参列表){
class 内部类的类名{
内部类的类体;
}
}
}
6)局部内部类的使用方式
- 局部内部类只能在该方法的内部可以使用。
- 局部内部类可以在方法体内部直接创建对象。
- 局部内部类不能使用访问控制符和static关键字修饰符。
- 局部内部类可以使用外部方法的局部变量,但是必须是final的。由局部内部类和局部变量的声明周期不同所致。
/**
* 局部内部类示例
*/
public class PartOuter {
private int ia = 1;
public void show() {
final int ib = 2; // 可以省略final关键字,但建议还是加上,从Java8开始默认理解为final关键字修饰的变量
// 定义局部内部类,只作用于当前方法
class PartInner {
// 声明局部内部类的变量
private int ic = 3;
public PartInner() {
System.out.println("局部内部类的构造方法");
}
public void test() {
System.out.println("ia = " + ia); // 1
// ib = 4; 编译报错,局部内类访问的外部方法的局部变量默认为被final修改
System.out.println("ib = " + ib); // 2
System.out.println("ic = " + ic); // 3
}
}
// 声明局部内部类的引用指向局部内部类的对象
PartInner pi = new PartInner();
pi.test();
}
}
/**
* 测试类
*/
public class PartOuterTest {
public static void main(String[] args) {
// 声明外部类类型的引用指向外部类的对象
PartOuter po = new PartOuter();
// 调用外部类的方法实现局部内部类的定义和使用
po.show();
}
}
7)匿名内部类的语法格式(重点)
接口/父类类型引用变量名 = new 接口/父类类型() { 方法的重写};
(五)回调模式的概念
回调模式是指:如果一个方法的参数是接口类型,则在调用该方法时,需要创建并传递一个实现此接口类型的对象;而该方法在运行时会调用到参数对象中所实现的方法(接口中定义的)。
建议:
- 当接口/类类型的引用作为方法的形参时,实参的传递方式有两种:
- 自定义类实现接口/继承类并重写方法,然后创建该类对象作为实参传递;
- 使用上述匿名内部类的语法格式得到接口/类类型的引用即可;
/**
* 接口类
*/
public interface AnonymousInterface {
public abstract void show();
}
/**
* 接口类的实现类
*/
public class AnonymousInterfaceImpl implements AnonymousInterface {
@Override
public void show() {
System.out.println("AnonymousInterface接口的实现类");
}
}
/**
* 匿名内部类的示例
*/
public class AnonymousInterfaceTest {
// 假设已有下面的方法
public static void test(AnonymousInterface ai) {
// 编译阶段调用父类版本,运行调用实现类重写的版本
ai.show();
}
public static void main(String[] args) {
// 接口类型的引用指向实现类型的对象,形成多态
AnonymousInterface ai = new AnonymousInterfaceImpl();
ai.show();
// 回调模式中实参的传递方式一
//AnonymousInterfaceTest.test(new AnonymousInterface()); // 报错,接口不能实例化
AnonymousInterfaceTest.test(new AnonymousInterfaceImpl()); // 实例化接口实现类,利用多态来调用test方法
// 回调模式中实参的传递方式二
// 使用匿名内部类的语法格式来获取接口类型的引用
AnonymousInterface ai2 = new AnonymousInterface() {
@Override
public void show() {
System.out.println("匿名内部类中重写的show()方法");
}
};
AnonymousInterfaceTest.test(ai2);
// Java8新特性lamda表达式可以简化上述代码,格式为:(参数列表) -> {方法体}
AnonymousInterface ai3 = () -> System.out.println("lamda表达式格式重写的show()方法");
AnonymousInterfaceTest.test(ai3);
}
}
二、枚举(熟悉)
(一)基本概念
在日常生活中有些事物的取值只有明确的几个固定值,此时描述这些事物的所有值都可以一一列举出来,而这个列举出来的类型就叫做枚举类型。
(二)定义
- 使用public static final表示的常量描述较为繁琐,使用enum关键字来定义枚举类型取代常量,枚举类型是从Java5开始增加的一种引用数据类型。
- 枚举值就是当前类的类型,也就是指向本类的对象,默认使用public static final关键字共同修饰,因此采用枚举类型.的方式调用。
- 枚举类可以自定义构造方法,但是构造方法的修饰符必须是private,默认也是私有的。
(三)Enum类的概念和方法
所有的枚举类都继承自java.lang.Enum类,常用方法如下:
(四)枚举类实现接口的方式
枚举类实现接口后需要重写抽象方法,而重写方法的方式有两种:重写一个,或者每个对象都重写。
/**
* 普通类写法
*/
public class Season {
private final String season; // 描述季节的私有成员变量
// 私有化构造方法,此时该构造方法只能在本类的内部使用
private Season(String season) {
this.season = season;
}
// 通过公有的get方法提供外部访问该类成员变量的数值
public String getSeason() {
return season;
}
// 声明本类类型的引用指向本类类型的对象
public static final Season SPRING = new Season("春天");
public static final Season SUMMER = new Season("夏天");
public static final Season AUTUMN = new Season("秋天");
public static final Season WINTER = new Season("冬天");
}
/**
* 接口类,自定义抽象方法
*/
public interface SeasonInterface {
public abstract void show();
}
/**
* 枚举类的示例
*/
public enum SeasonEnum implements SeasonInterface {
// 枚举类型要求所有枚举值都必须放在最前面
SPRING("春天") {
@Override
public void show() {
System.out.println("春天到了");
}
},
SUMMER("夏天") {
@Override
public void show() {
System.out.println("春天到了");
}
},
AUTUMN("秋天") {
@Override
public void show() {
System.out.println("秋天到了");
}
},
WINTER("冬天") {
@Override
public void show() {
System.out.println("冬天到了");
}
};
// 私有成员变量
private final String season;
// 私有化构造方法,该构造方法只能在本类的内部使用
private SeasonEnum(String season) {
this.season = season;
}
// 通过公有的get方法提供外部类访问成员变量的数值
public String getSeason() {
return season;
}
}
public class SeasonTest {
// 自定义静态方法实现根据参数指定的字符串内容来打印具体的信息
public static void test1(String str) {
switch (str) {
case "春天":
System.out.println("春暖花开");break;
case "夏天":
System.out.println("烈日当空");break;
case "秋天":
System.out.println("秋高气爽");break;
case "冬天":
System.out.println("天寒地冻");break;
default:
System.out.println("不是四季");
}
}
// 自定义静态方法实现根据参数指定的枚举类型来打印具体的信息
public static void test2(SeasonEnum se) {
switch (se) {
case SPRING:
System.out.println("春暖花开");break;
case SUMMER:
System.out.println("烈日当空");break;
case AUTUMN:
System.out.println("秋高气爽");break;
case WINTER:
System.out.println("天寒地冻");break;
default:
System.out.println("不是四季");
}
}
public static void main(String[] args) {
Season s = Season.SPRING;
System.out.println(s.getSeason());
SeasonTest.test1(s.getSeason());
System.out.println("-----------------------------");
// 使用枚举类型
SeasonEnum se = SeasonEnum.SUMMER;
System.out.println(se.getSeason());
SeasonTest.test2(se);
}
}
/**
* 枚举类的测试,调用从Enum类中继承下来的方法
*/
public class SeasonEnumTest {
public static void main(String[] args) {
// 获取SeasonEnum类型中所有的枚举对象
SeasonEnum[] arr = SeasonEnum.values();
// 打印每个枚举对象在枚举类型中的名称和索引位置
for (int i = 0; i < arr.length; i++) {
System.out.println("此时枚举对象名称是:" + arr[i].toString());
System.out.println("此时的枚举对象对应的索引位置是:" + arr[i].ordinal()); // 和数组一样下标从0开始
}
System.out.println("----------------------------------------------------");
// 根据参数指定的字符串得到枚举类型的对象,也就是将字符串转换为对象
// SeasonEnum se = SeasonEnum.valueOf("春天"); // 编译正常,运行报IllegalArgumentException非法参数异常,因为找不到以春天为名的枚举对象
SeasonEnum se = SeasonEnum.valueOf("SUMMER");
System.out.println("转换出来的枚举对象名称是:" + se.toString()); // toString()可以省略,当打印引用变量时,会自动调用toString方法
System.out.println("----------------------------------------------------");
// 使用获取到的枚举对象与枚举类中已有的对象比较先后顺序
for (int i = 0; i < arr.length; i++) {
// 当调用对象在参数对象之后时,获取到的比较结果为 正数;同一位置时则为 0;在参数对象之前时则为 负数
System.out.print(se.compareTo(arr[i]) + " ");
}
System.out.println();
System.out.println("----------------------------------------------------");
// 调用每一个枚举对象的show方法
for (int i = 0; i < arr.length; i++) {
arr[i].show();
}
}
}
三、注解(重点)
(一)基本概念
- 注解(Annotation)又叫标注,是从Java5开始增加的一种引用数据类型。
- 注解本质上就是代码中的特殊标记,通过这些标记可以在编译、类加载、以及运行时执行指定的处理。
(二)注解的语法格式
访问修饰符 @interface 注解名称{
注解成员;
}
- 自定义注解自动继承java.lang.annotation.Annotation接口。
- 通过@注解名称的方式可以修饰包、类、成员方法、成员变量、构造方法、参数、局部变量的声明等。
(三)使用方式
- 注解体中只有成员变量没有成员方法,而注解的成员变量以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。
- 如果注解只有一个参数成员,建议使用参数名为value,而类型只能是八种基本数据类型、String类型、Class类型、enum类型及Annotation类型。
(四)元注解的概念
- 元注解是可以注解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。
- 元注解主要有@Retention、@Documented、@Target、@Inherited、@Repeatable。
1)元注解@Retention
- @Retention 应用到一个注解上用于说明该注解的的生命周期,取值如下:
- RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
- RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到JVM 中,默认方式。
- RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到JVM 中,所以在程序运行时可以获取到它们。
2)元注解@Documented
- 使用javadoc工具可以从程序源代码中抽取类、方法、成员等注释形成一个和源代码配套的API帮助文档,而该工具抽取时默认不包括注解内容。
- @Documented用于指定被该注解将被javadoc工具提取成文档。
- 定义为@Documented的注解必须设置Retention值为RUNTIME。
3)元注解@Target
@Target用于指定被修饰的注解能用于哪些元素的修饰,取值如下:
4)元注解@Inherited
@Inherited并不是说注解本身可以继承,而是说如果一个超类被该注解标记过的注解进行注解时,如果子类没有被任何注解应用时,则子类就继承超类的注解。
5)元注解@Repeatable
- @Repeatable表示自然可重复的含义,从Java8开始增加的新特性。
- 从Java8开始对元注解@Target的参数类型ElementType枚举值增加了两个:
- 其中ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中,如:泛型。
- 其中ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中。
(四)常见的预制注解
预制注解就是Java语言自身提供的注解,具体如下:
import java.lang.annotation.*;
/**
* 自定义注解里面可以描述多种角色
*/
@Documented // 表示下面的注解信息可以被javadoc工具提取到API文档中,很少使用
//@Retention(RetentionPolicy.SOURCE) // 表示下面的注解在源代码中有效
//@Retention(RetentionPolicy.CLASS) // 表示下面的注解在字节码文件中有效,默认方式
@Retention(RetentionPolicy.RUNTIME) // 表示下面的注解在运行时有效
@Target({ElementType.TYPE_USE}) // 该注解能写在使用类型的任何语句中
@Inherited // 表示下面的注解所修饰的类中的注解使用可以被子类继承
public @interface ManTypes {
ManType[] value();
}
import java.lang.annotation.*;
/**
* 自定义注解用于描述任务的角色
*/
@Documented
@Repeatable(value = ManTypes.class) // 使用该元注解后,同一类型上可以使用相同的该注解
@Retention(RetentionPolicy.RUNTIME) // ManTypes中若不设置为运行时有效,此处不能设置为RUNTIME,只能设置为与ManTypes相同或小于的有效范围
// 表示下面定义的注解可以用于类型、构造方法、成员变量、成员方法、参数 的修饰
//@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Target(ElementType.TYPE_USE) // 表示该注解能写在使用类型的任何语句中
@Inherited // 表示下面的注解所修饰的类中的注解使用可以被子类继承
// 若一个注解中没有任何的成员,则这样的注解叫做标记注解/标识注解
public @interface ManType {
String value() default ""; // 默认值为空
}
// 表示将标签 ManType 贴在 Man 类的代码中,使用注解时采用 成员参数名 = 成员参数值的方式,有多个参数时用","隔开
@ManType(value = "老师")
@ManType(value = "医生") // 使用@Repeatable注解的注解后,同一类型上可以使用多次使用该注解
//@ManTypes({@ManType(value = "老师"), @ManType(value = "医生")}) // 在Java8以前处理多个注解的方式
public class Man {
@Deprecated // 表示该方法已经过时
public void show() {
System.out.println("此方法已过时,不建议再使用,并不代表不能用");
}
public static void main(String[] args) {
int a = 97;
char c = (@ManType char) a;
}
}
public class ManTest {
public static void main(String[] args) {
Man man = new Man();
man.show(); // 带横线,表示该方法已过时,不建议使用
}
}