本文介绍了Java
面向对象
的一些基本知识,对面向对象的三大特征(继承
、封装
、多态
)进行了讲解,另外还包括抽象类
、接口
、内部类
、String
基础等知识。
类的定义方式
- 每一个源文件必须有且只有一个
public class
,并且类名和文件名保持一致! - 一个
Java
文件可以同时定义多个class
1 | // 每一个源文件必须有且只有一个public class,并且类名和文件名保持一致! |
面向对象的内存分析
Java虚拟机
的内存可以分为三个区域:栈 stack
、堆 heap
、方法区 method area
。
栈的特点如下:
- 栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧(存储局部变量、操作数、方法出口等)
- JVM为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变量等)
- 栈属于线程私有,不能实现线程间的共享!
- 栈的存储特性是“先进后出,后进先出”
- 栈是由系统自动分配,速度快!栈是一个连续的内存空间!
堆的特点如下:
- 堆用于存储创建好的对象和数组(数组也是对象)
- JVM只有一个堆,被所有线程共享
- 堆是一个不连续的内存空间,分配灵活,速度慢!
方法区(又叫静态区
)特点如下:
- JVM只有一个方法区,被所有线程共享!
- 方法区实际也是堆,只是用于存储类、常量相关的信息!
- 用来存放程序中永远是不变或唯一的内容。(类信息【Class对象】、静态变量、字符串常量等)
- 内存分配图
构造方法
构造器
也叫构造方法(constructor)
,用于对象的初始化。构造器是一个创建对象时被自动调用的特殊方法,目的是对象的初始化。构造器的名称应与类的名称一致。Java通过new
关键字来调用构造器,从而返回该类的实例,是一种特殊的方法。
声明格式:
1 | [修饰符] 类名(形参列表){ |
要点:
- 通过
new
关键字调用!! - 构造器虽然有返回值,但是不能定义返回值类型(返回值的类型肯定是本类),不能在构造器里使用
return
返回某个值。 - 如果我们没有定义构造器,则编译器会自动定义一个无参的构造函数。如果已定义则编译器不会自动添加!
- 构造器的方法名必须和类名一致!
- 构造方法也是方法,只不过有特殊的作用而已。与普通方法一样,构造方法也可以重载。
- 如果方法构造中形参名与属性名相同时,需要使用
this
关键字区分属性与形参。
垃圾回收机制
Java引入了
垃圾回收机制(Garbage Collection)
,令C++程序员最头疼的内存管理问题迎刃而解。Java程序员可以将更多的精力放到业务逻辑上而不是内存管理工作上,大大的提高了开发效率。
垃圾回收原理和算法
- 内存管理
Java的内存管理很大程度指的就是对象的管理,其中包括对象空间的分配和释放。
- 对象空间的分配:使用new关键字创建对象即可
- 对象空间的释放:将对象赋值null即可。垃圾回收器将负责回收所有”不可达”对象的内存空间。
- 垃圾回收过程
任何一种垃圾回收算法一般要做两件基本事情:
- 发现无用的对象
- 回收无用对象占用的内存空间。
垃圾回收机制保证可以将“无用的对象”进行回收。无用的对象指的就是没有任何变量引用该对象。Java的垃圾回收器通过相关算法发现无用对象,并进行清除和整理。
- 垃圾回收相关算法
- 引用计数法
堆中每个对象都有一个引用计数。被引用一次,计数加1. 被引用变量值变为null,则计数减1,直到计数为0,则表示变成无用对象。优点是算法简单,缺点是“循环引用的无用对象”无法别识别。
示例:循环引用示例
1 | public class Student { |
s1和s2互相引用对方,导致他们引用计数不为0,但是实际已经无用,但无法被识别。
- 引用可达法(根搜索算法)
程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
开发中容易造成内存泄露的操作
如下四种情况时最容易造成内存泄露的场景,开发时一定注意:
- 创建大量无用对象
比如,我们在需要大量拼接字符串时,使用了String而不是StringBuilder。
1 | String str = ""; |
- 静态集合类的使用
像HashMap、Vector、List等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放。
- 各种连接对象(IO流对象、数据库连接对象、网络连接对象)未关闭
IO流对象、数据库连接对象、网络连接对象等连接对象属于物理连接,和硬盘或者网络连接,不使用的时候一定要关闭。
4.监听器的使用
释放对象时,没有删除相应的监听器。
要点:
- 程序员无权调用垃圾回收器。
- 程序员可以调用
System.gc()
,该方法只是通知JVM
,并不是运行垃圾回收器。尽量少用,会申请启动Full GC
,成本高,影响系统性能。 finalize
方法,是Java提供给程序员用来释放对象或资源的方法,但是尽量少用。
this关键字
对象创建的过程和this的本质
构造方法是创建 Java 对象的重要途径,通过
new
关键字调用构造器时,构造器也确实返回该类的对象,但这个对象并不是完全由构造器负责创建。创建一个对象分为如下四步:
- 分配对象空间,并将对象成员变量初始化为0或空
- 执行属性值的显示初始化
- 执行构造方法
- 返回对象的地址给相关的变量
this的本质就是“创建好的对象的地址”! 由于在构造方法调用前,对象已经创建。因此,在构造方法中也可以使用this代表“当前对象” 。
this最常的用法:
- 在程序中产生二义性之处,应使用this来指明当前对象;普通方法中,this总是指向调用该方法的对象。构造方法中,this总是指向正要初始化的对象。
- 使用this关键字调用重载的构造方法,避免相同的初始化代码。但只能在构造方法中用,并且必须位于构造方法的第一句。
- this不能用于static方法中。
示例:this代表“当前对象”示例
1 | public class User { |
运行结果如下图所示。
示例:this()调用重载构造方法
1 | public class TestThis { |
static 关键字
在类中,用
static
声明的成员变量为静态成员变量,也称为类变量。 类变量的生命周期和类相同,在整个应用程序执行期间都有效。它有如下特点:
- 为该类的公用变量,属于类,被该类的所有实例共享,在类被载入时被显式初始化。
- 对于该类的所有对象来说,static成员变量只有一份。被该类的所有对象共享!!
- 一般用“类名.类属性/方法”来调用。(也可以通过对象引用或类名(不需要实例化)访问静态成员。)
- 在static方法中不可直接访问非static的成员。
核心要点:
- static修饰的成员变量和方法,从属于类。
- 普通变量和方法从属于对象的。
示例:static关键字的使用
1 | /** |
运行结果如下图所示。
静态初始化块
构造方法
用于对象的初始化!静态初始化块
,用于类的初始化操作!在静态初始化块中不能直接访问非static成员。
注意事项:
静态初始化块执行顺序:
- 上溯到Object类,先执行Object的静态初始化块,再向下执行子类的静态初始化块,直到我们的类的静态初始化块为止。
- 构造方法执行顺序和上面顺序一样!
示例:static初始化块
1 | public class User3 { |
执行结果如下图所示。
参数传值机制
Java中,方法中所有参数都是“值传递”,也就是“传递的是值的副本”。 也就是说,我们得到的是“原参数的复印件,而不是原件”。因此,复印件改变不会影响原件。
- 基本数据类型参数的传值
传递的是值的副本。 副本改变不会影响原件。
- 引用类型参数的传值
传递的是值的副本。但是引用类型指的是“对象的地址”。因此,副本和原参数都指向了同一个“地址”,改变“副本指向地址对象的值,也意味着原参数指向对象的值也发生了改变”。
示例:多个变量指向同一个对象
1 | /** |
执行结果如下图所示。
包(package)
包机制
是Java中管理类的重要手段。 开发中,我们会遇到大量同名的类,通过包我们很容易对解决类重名的问题,也可以实现对类的有效管理。 包对于类,相当于文件夹对于文件的作用。
我们通过package实现对类的管理,package的使用有两个要点:
通常是类的第一句非注释性语句。
包名:域名倒着写即可,再加上模块名,便于内部管理类。
示例:package的命名举例
1 | com.sun.test; |
注意事项:
- 写项目时都要加包,不要使用默认包。
- com.gao和com.gao.car,这两个包没有包含关系,是两个完全独立的包。只是逻辑上看起来后者是前者的一部分。
示例:package的使用
1 | package cn.sxt; |
JDK中的主要包
Java中的常用包 | 说明 |
---|---|
java.lang | 包含一些Java语言的核心类,如String、Math、Integer、System和Thread,提供常用功能。 |
java.awt | 包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。 |
java.net | 包含执行与网络相关的操作的类。 |
java.io | 包含能提供多种输入/输出功能的类。 |
java.util | 包含一些实用工具类,如定义系统特性、使用与日期日历相关的函数。 |
导入类import
如果我们要使用其他包的类,需要使用
import
导入,从而可以在本类中直接通过类名来调用,否则就需要书写类的完整包名和类名。import后,便于编写代码,提高可维护性。
注意要点:
- Java会默认导入
java.lang
包下所有的类,因此这些类我们可以直接使用。 - 如果导入两个同名的类,只能用包名+类名来显示调用相关类:
1 | java.util.Date date = new java.util.Date(); |
示例:导入同名类的处理
1 | import java.sql.Date; |
静态导入
静态导入(static import)
是在JDK1.5新增加的功能,其作用是用于导入指定类的静态属性,这样我们可以直接使用静态属性。
示例:静态导入的使用
1 | package cn.sxt; |
执行结果如下图所示。
继承
继承的实现
示例:使用extends实现继承
1 | public class Test{ |
instanceof 运算符
instanceof是
二元运算符
,左边是对象,右边是类;当对象是右面类或子类所创建对象时,返回true;否则,返回false。比如:
示例:使用instanceof运算符进行类型判断
1 | public class Test{ |
两条语句的输出结果都是true。
继承使用要点
父类
也称作超类
、基类
、派生类
等。- Java中只有
单继承
,没有像 C++ 那样的多继承
。多继承会引起混乱,使得继承链过于复杂,系统难于维护。 - Java中
类
没有多继承,接口
有多继承。 子类
继承父类
,可以得到父类的全部属性和方法 (除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)。- 如果定义一个类时,没有调用
extends
,则它的父类是:java.lang.Object
。
方法的重写override
子类通过重写父类的方法,可以用自身的行为替换父类的行为。方法的重写是实现
多态
的必要条件。
方法的重写需要符合下面的三个要点:
- “==”: 方法名、形参列表相同。
- “≤”:返回值类型和声明异常类型,子类小于等于父类。
- “≥”: 访问权限,子类大于等于父类。
示例:方法重写
1 | public class TestOverride { |
执行结果如下图所示:
Object类基本特性
Object类是所有Java类的
根基类
,也就意味着所有的Java对象都拥有Object类的属性和方法。如果在类的声明中未使用extends关键字指明其父类,则默认继承Object类。
示例:Object类
1 | public class Person { |
toString方法
Object类中定义有public String toString()方法,其返回值是 String 类型。Object类中toString方法的源码为:
1 | public String toString() { |
根据如上源码得知,默认会返回“类名+@+16进制的hashcode”。在打印输出或者用字符串连接对象时,会自动调用该对象的toString()方法。
示例:toString()方法测试和重写toString()方法
1 | class Person { |
执行结果如下图所示:
==和equals方法
“==”代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象。
Object类中定义有:public boolean equals(Object obj)方法,提供定义“对象内容相等”的逻辑。比如,我们在公安系统中认为id相同的人就是同一个人、学籍系统中认为学号相同的人就是同一个人。
Object 的 equals 方法默认就是比较两个对象的
hashcode
,是同一个对象的引用时返回 true 否则返回 false。但是,我们可以根据我们自己的要求重写equals方法。
示例:equals方法测试和自定义类重写equals方法
1 | public class TestEquals { |
JDK提供的一些类,如String、Date、包装类等,重写了Object的equals方法,调用这些类的equals方法, x.equals (y) ,当x和y所引用的对象是同一类对象且属性内容相等时(并不一定是相同对象),返回 true 否则返回 false。
super关键字
定义及其使用
super
是直接父类对象的引用。可以通过super来访问父类中被子类覆盖的方法或属性。使用super调用普通方法,语句没有位置限制,可以在子类中随便调用。
若是构造方法的第一行代码没有显式的调用
super(...)
或者this(...)
;那么Java默认都会调用super(),含义是调用父类的无参数构造方法。这里的super()可以省略。
示例:super关键字的使用
1 | public class TestSuper01 { |
执行结果如下图所示:
继承树追溯
- 属性/方法查找顺序:(比如:查找变量h)
- 查找当前类中有没有属性h
- 依次上溯每个父类,查看每个父类中是否有h,直到Object
- 如果没找到,则出现编译错误。
- 上面步骤,只要找到h变量,则这个过程终止。
- 构造方法调用顺序:
构造方法第一句总是:super(…)来调用父类对应的构造方法。所以,流程就是:先向上追溯到Object,然后再依次向下执行类的初始化块和构造方法,直到当前子类为止。
注:静态初始化块调用顺序,与构造方法调用顺序一样,不再重复。
示例:构造方法向上追溯执行测试
1 | public class TestSuper02 { |
执行结果如下图所示:
封装
需要让用户知道的才暴露出来,不需要让用户知道的全部隐藏起来,这就是封装。说的专业一点,封装就是把对象的属性和操作结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。
我们程序设计要追求“高内聚,低耦合”。 高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。
封装的优点
- 提高代码的安全性。
- 提高代码的复用性。
- “高内聚”:封装细节,便于修改内部代码,提高可维护性。
- “低耦合”:简化外部调用,便于调用者使用,便于扩展和协作。
示例:没有封装的代码会出现一些问题
1 | class Person { |
我们都知道,年龄不可能是负数,也不可能超过130岁,但是如果没有使用封装的话,便可以给年龄赋值成任意的整数,这显然不符合我们的正常逻辑思维。执行结果如下图所示:
再比如说,如果哪天我们需要将Person类中的age属性修改为String类型的,你会怎么办?你只有一处使用了这个类的话那还比较幸运,但如果你有几十处甚至上百处都用到了,那你岂不是要改到崩溃。而封装恰恰能解决这样的问题。如果使用封装,我们只需要稍微修改下Person类的setAge()方法即可,而无需修改使用了该类的客户代码。
封装的实现
Java是使用“
访问控制符
”来控制哪些细节需要封装,哪些细节需要暴露的。 Java中4种“访问控制符”分别为private、default、protected、public,它们说明了面向对象的封装性,所以我们要利用它们尽可能的让访问权限降到最低,从而提高安全性。下面详细讲述它们的访问权限问题。其访问权限范围如下表所示。
修饰符 | 同一个类 | 同一个包 | 子类 | 所有类 |
---|---|---|---|---|
private | √ | |||
default | √ | √ | ||
protected | √ | √ | √ | |
public | √ | √ | √ | √ |
- private 表示私有,只有自己类能访问
- default表示没有修饰符修饰,只有同一个包的类能访问
- protected表示可以被同一个包的类以及其他包中的子类访问
- public表示可以被该项目的所有包中的所有类访问
下面做进一步说明Java中4种访问权限修饰符的区别:首先我们创建4个类:Person类、Student类、Animal类和Computer类,分别比较本类、本包、子类、其他包的区别。
- public访问权限修饰符:
public访问权限—本类中访问public属性:
public访问权限—本包中访问public属性:
public访问权限—不同包中的子类访问public属性:
public访问权限—不同包中的非子类访问public属性:
通过上四张图可以说明,public修饰符的访问权限为:该项目的所有包中的所有类。
- protected访问权限修饰符:将Person类中属性改为protected,其他类不修改。
protected访问权限—修改后的Person类:
protected访问权限—不同包中的非子类不能访问protected属性:
通过上两张图可以说明,protected修饰符的访问权限为:同一个包中的类以及其他包中的子类。
- 默认访问权限修饰符:将Person类中属性改为默认的,其他类不修改。
默认访问权限—修改后的Person类:
通过上图可以说明,默认修饰符的访问权限为:同一个包中的类。
- private访问权限修饰符:将Person类中属性改为private,其他类不修改。
private访问权限—修改后的Person类:
通过上图可以说明,private修饰符的访问权限为:同一个类。
封装的使用细节
类的属性的处理:
- 一般使用private访问权限。
- 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头!)。
- 一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰。
示例:JavaBean的封装实例
1 | public class Person { |
下面我们使用封装来解决一下上面提到的年龄非法赋值的问题。
示例:封装的使用
1 | class Person { |
执行结果如下图所示:
多态
多态指的是同一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个方法,具体实现会完全不同。 比如:同样是调用人的“休息”方法,张三是睡觉,李四是旅游,高淇老师是敲代码,数学教授是做数学题; 同样是调用人“吃饭”的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。
多态的要点:
- 多态是方法的多态,不是属性的多态(多态与属性无关)。
- 多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象。
- 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。
示例:多态和类型转换测试
1 | class Animal { |
执行结果如下图所示:
上例给大家展示了多态最为多见的一种用法,即父类引用做方法的形参,实参可以是任意的子类对象,可以通过不同的子类对象实现不同的行为方式。
由此,我们可以看出多态的主要优势是提高了代码的可扩展性,符合开闭原则。但是多态也有弊端,就是无法调用子类特有的功能,比如,我不能使用父类的引用变量调用Dog类特有的seeDoor()方法。
那如果我们就想使用子类特有的功能行不行呢?行!这就是我们下一章节所讲的内容:对象的转型。
对象的转型
父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。
向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方法。这时,我们就需要进行强制类型转换,我们称之为向下转型!
示例:对象的转型
1 | public class TestCasting { |
执行结果如下图所示:
在向下转型过程中,必须将引用变量转成真实的子类类型(运行时类型)否则会出现类型转换异常ClassCastException。如下例所示。
示例:类型转换异常
1 | public class TestCasting2 { |
执行结果如下图所示:
为了避免出现这种异常,我们可以使用instanceof运算符进行判断,如下例所示。
示例:向下转型中使用instanceof
1 | public class TestCasting3 { |
执行结果如下图所示:
final关键字
final关键字的作用:
- 修饰变量: 被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。
1 | final int MAX_SPEED = 120; |
- 修饰方法:该方法不可被子类重写。但是可以被重载!
1 | final void study(){} |
- 修饰类: 修饰的类不能被继承。比如:Math、String等。
1 | final class A {} |
final修饰方法如下图所示。
final修饰类如下图所示。
抽象方法和抽象类
- 抽象方法
使用abstract修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。
- 抽象类
包含抽象方法的类就是抽象类。通过abstract
示例:抽象类和抽象方法的基本用法
1 | //抽象类 |
抽象类的使用要点:
- 有抽象方法的类只能定义成抽象类
- 抽象类不能实例化,即不能用new来实例化抽象类。
- 抽象类可以包含属性、方法、构造方法。但是构造方法不能用来new实例,只能用来被子类调用。
- 抽象类只能用来被继承。
- 抽象方法必须被子类实现。
接口
接口的定义和使用
声明格式:
1 | [访问修饰符] interface 接口名 [extends 父接口1,父接口2…] { |
定义接口的详细说明:
- 访问修饰符:只能是public或默认。
- 接口名:和类名采用相同命名机制。
- extends:接口可以多继承。
- 常量:接口中的属性只能是常量,总是:public static final 修饰。不写也是。
- 方法:接口中的方法只能是:public abstract。 省略的话,也是public abstract。
要点:
- 子类通过implements来实现接口中的规范。
- 接口不能创建实例,但是可用于声明引用变量类型。
- 一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是public的。
- JDK1.7之前,接口中只能包含静态常量、抽象方法,不能有普通属性、构造方法、普通方法。
- JDK1.8后,接口中包含普通的静态方法。
示例:接口的使用
1 | public class TestInterface { |
执行结果如下图所示:
接口的多继承
接口完全支持多继承。和类的继承类似,子接口扩展某个父接口,将会获得父接口中所定义的一切。
示例:接口的多继承
1 | interface A { |
内部类
内部类介绍
一般情况,我们把类定义成独立的单元。有些情况下,我们把一个类放在另一个类的内部定义,称为内部类(innerclasses)。
内部类可以使用public、default、protected 、private以及static修饰。而外部顶级类(我们以前接触的类)只能使用public和default修饰。
注意:
内部类只是一个编译时概念,一旦我们编译成功,就会成为完全不同的两个类。对于一个名为Outer的外部类和其内部定义的名为Inner的内部类。编译完成后会出现Outer.class和Outer$Inner.class两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类的相同。
示例:内部类介绍
1 | /**外部类Outer*/ |
上例编译后会产生两个不同的字节码文件,如下图所示:
内部类的作用
- 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。
- 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。 但外部类不能访问内部类的内部属性。
- 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方案变得更加完整。
内部类的使用场合
- 由于内部类提供了更好的封装特性,并且可以很方便的访问外部类的属性。所以,在只为外部类提供服务的情况下可以优先考虑使用内部类。
- 使用内部类间接实现多继承:每个内部类都能独立地继承一个类或者实现某些接口,所以无论外部类是否已经继承了某个类或者实现了某些接口,对于内部类没有任何影响。
内部类的分类
在Java中内部类主要分为
成员内部类
(非静态内部类
、静态内部类
)、匿名内部类
、局部内部类
。
成员内部类
可以使用private、default、protected、public任意进行修饰。
类文件:外部类$内部类.class
非静态内部类
外部类里使用非静态内部类和平时使用其他类没什么不同
非静态内部类必须寄存在一个外部类对象里。因此,如果有一个非静态内部类对象那么一定存在对应的外部类对象。非静态内部类对象单独属于外部类的某个对象。
非静态内部类可以直接访问外部类的成员,但是外部类不能直接访问非静态内部类成员。
非静态内部类不能有静态方法、静态属性和静态初始化块。
外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例。
成员变量访问要点:
- 内部类里方法的局部变量:变量名。
- 内部类属性:this.变量名。
- 外部类属性:外部类名.this.变量名。
示例:成员变量的访问要点
1 | class Outer { |
- 内部类的访问:
- 外部类中定义内部类:
1 | new Inner() |
- 外部类以外的地方使用非静态内部类:
1 | Outer.Inner varname = new Outer().new Inner()。 |
示例:内部类的访问
1 | public class TestInnerClass { |
执行结果如下图所示:
静态内部类
- 定义方式:
1 | static class ClassName { |
- 使用要点:
- 当一个静态内部类对象存在,并不一定存在对应的外部类对象。 因此,静态内部类的实例方法不能直接访问外部类的实例方法。
- 静态内部类看做外部类的一个静态成员。 因此,外部类的方法中可以通过:“静态内部类.名字”的方式访问静态内部类的静态成员,通过 new 静态内部类()访问静态内部类的实例。
示例:静态内部类的访问
1 | class Outer{ |
匿名内部类
适合那种只需要使用一次的类。比如:键盘监听操作等等。
语法:
1 | new 父类构造器(实参类表) \实现接口 () { |
示例:匿名内部类的使用
1 | this.addWindowListener(new WindowAdapter(){ |
注意:
- 匿名内部类没有访问修饰符。
- 匿名内部类没有构造方法。因为它连名字都没有那又何来构造方法呢。
局部内部类
还有一种内部类,它是定义在方法内部的,作用域只限于本方法,称为局部内部类。
局部内部类的的使用主要是用来解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类。局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法中被使用,出了该方法就会失效。
局部内部类在实际开发中应用很少。
示例:方法中的内部类
1 | public class Test2 { |
执行结果如下图所示:
String基础
基础知识
- String类又称作不可变字符序列。
- String位于java.lang包中,Java程序默认导入java.lang包下的所有类。
- Java字符串就是Unicode字符序列,例如字符串“Java”就是4个Unicode字符’J’、’a’、’v’、’a’组成的。
- Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义的类String,每个用双引号括起来的字符串都是String类的一个实例。
示例:String类的实例
1 | String e = "" ; // 空字符串 |
- Java允许使用符号”+”把两个字符串连接起来。
示例:字符串连接
1 | String s1 = "Hello"; |
- 符号”+”把两个字符串按给定的顺序连接在一起,并且是完全按照给定的形式。
- 当”+”运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串然后再进行连接。
示例:”+”连接符
1 | int age = 18; |
String类和常量池
在Java的内存分析中,我们会经常听到关于“常量池”的描述,实际上常量池也分了以下三种:
- 全局字符串常量池(String Pool)
全局字符串常量池中存放的内容是在类加载完成后存到String Pool中的,在每个VM中只有一份,存放的是字符串常量的引用值(在堆中生成字符串对象实例)。
- class文件常量池(Class Constant Pool)
class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量(文本字符串、final常量等)和符号引用。
- 运行时常量池(Runtime Constant Pool)
运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
示例:常量池
1 | String str1 = "abc"; |
上例中经过编译之后,在该类的
class常量池
中存放一些符号引用,然后类加载之后,将class常量池
中存放的符号引用转存到运行时常量池
中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的“abc”实例对象),然后将这个对象的引用存到全局String Pool中,也就是String Pool中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询String Pool,保证String Pool里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。回到上例的程序,现在就很容易解释整个程序的内存分配过程了,首先,在堆中会有一个“abc”实例,全局String Pool中存放着“abc”的一个引用值,然后在运行第二句的时候会生成两个实例,一个是“def”的实例对象,并且String Pool中存储一个“def”的引用值,还有一个是new出来的一个“def”的实例对象,与上面那个是不同的实例,当在解析str3的时候查找String Pool,里面有“abc”的全局驻留字符串引用,所以str3的引用地址与之前的那个已存在的相同,str4是在运行的时候调用intern()函数,返回String Pool中“def”的引用值,如果没有就将str2的引用值添加进去,在这里,String Pool中已经有了“def”的引用值了,所以返回上面在new str2的时候添加到String Pool中的 “def”引用值,最后str5在解析的时候就也是指向存在于String Pool中的“def”的引用值,那么这样一分析之后,结果就容易理解了。
String类常用的方法
方法 | 解释说明 |
---|---|
char charAt(int index) | 返回字符串中第index个字符 |
boolean equals(String other) | 如果字符串与other相等,返回true;否则,返回false。 |
boolean equalsIgnoreCase(String other) | 如果字符串与other相等(忽略大小写),则返回true;否则,返回false。 |
int indexOf(String str) | 返回从头开始查找第一个子字符串str在字符串中的索引位置。如果未找到子字符串str,则返回-1。 |
lastIndexOf() | 返回从末尾开始查找第一个子字符串str在字符串中的索引位置。如果未找到子字符串str,则返回-1。 |
int length() | 返回字符串的长度。 |
String replace(char oldChar,char newChar) | 返回一个新串,它是通过用newChar替换此字符串中出现的所有oldChar而生成的。 |
boolean startsWith(String prefix) | 如果字符串以prefix开始,则返回true。 |
boolean endsWith(String prefix) | 如果字符串以prefix结尾,则返回true。 |
String substring(int beginIndex) | 返回一个新字符串,该串包含从原始字符串beginIndex到串尾。 |
String substring(int beginIndex,int endIndex) | 返回一个新字符串,该串包含从原始字符串beginIndex到串尾或endIndex-1的所有字符。 |
String toLowerCase() | 返回一个新字符串,该串将原始字符串中的所有大写字母改成小写字母。 |
String toUpperCase() | 返回一个新字符串,该串将原始字符串中的所有小写字母改成大写字母。 |
String trim() | 返回一个新字符串,该串删除了原始字符串头部和尾部的空格。 |
示例:String类常用方法一
1 | public class StringTest1 { |
执行结果如下图所示:
示例:String类常用方法二
1 | public class StringTest2 { |
执行结果如下图所示:
字符串相等的判断
- equals方法用来检测两个字符串内容是否相等。如果字符串s和t内容相等,则s.equals(t)返回true,否则返回false。
- 要测试两个字符串除了大小写区别外是否是相等的,需要使用equalsIgnoreCase方法。
- 判断字符串是否相等不要使用”==”。
示例:忽略大小写的字符串比较
1 | "Hello".equalsIgnoreCase("hellO");//true |
示例:字符串的比较”==”与equals()方法
1 | public class TestStringEquals { |
执行结果如下图所示:
上例的内存分析如下图所示:
开闭原则
开闭原则(Open-Closed Principle)就是让设计的系统对扩展开放,对修改封闭。
- 对扩展开放:
就是指,应对需求变化要灵活。 要增加新功能时,不需要修改已有的代码,增加新代码即可。
- 对修改封闭:
就是指,核心部分经过精心设计后,不再因为需求变化而改变。
在实际开发中,我们无法完全做到,但应尽量遵守开闭原则。
模板方法模式和回调机制
模板方法模式很常用,其目的是在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。在标准的模板方法模式实现中,主要是使用继承的方式,来让父类在运行期间可以调用到子类的方法。 详见抽象类部分示例。
其实在Java开发中,还有另外一个方法可以实现同样的功能,那就是Java回调技术。回调是一种双向的调用模式,也就是说,被调用的接口被调用时也会调用对方的接口,简单点说明就是:A类中调用B类中的C方法,然后B类中的C方法中反过来调用A类中的D方法,那么D这个方法就叫回调方法。
回调的具体过程如下:
- Class A实现接口CallBack —— 背景1
- class A中包含class B的引用 ——背景2
- class B有一个参数为CallBack的方法C ——背景3
- 前三条是我们的准备条件,接下来A的对象调用B的方法C
- 然后class B就可以在C方法中调用A的方法D
这样说大家可能还是不太理解,下面我们根据下例来说明回调机制。该示例的生活背景为:有一天小刘遇到一个很难的问题“学习Java选哪家机构呢?”,于是就打电话问小高,小高一时也不太了解行情,就跟小刘说,我现在还有事,等忙完了给你咨询咨询,小刘也不会傻傻的拿着电话去等小高的答案,于是小刘对小高说,先挂电话吧,你知道答案后再打我电话告诉我吧,于是挂了电话。小高先去办自己的事情去了,过了几个小时,小高打电话给小刘,告诉他答案是“学Java当然去……”。
示例:回调机制示例
1 | /** |
执行结果如下图所示:
通过回调在接口中定义的方法,调用到具体的实现类中的方法,其本质是利用Java的动态绑定技术,在这种实现中,可以不把实现类写成单独的类,而使用内部类或匿名内部类来实现回调方法。
组合模式
组合模式是将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
示例:对象的组合
1 | class Cpu { |
执行结果如下图所示: