面向对象(下)
本文为Java面向对象详细提纲,源于《疯狂Java讲义 th5》
所用JDK版本为:java 11.0.2
作者:房梁上的猫
邮箱:gyqc@outlook.coom
包装类
-
通过包装类可以把8个基本类型的值包装成对象使用。
-
自动拆箱、自动装箱
-
把字符串类型值转换成基本类型的值:
- 包装类的
parseXxx(String s)
静态方法 - 包装类的
valueOf(String s)
静态方法
- 包装类的
-
包装类还提供多个重载
valueOf
方法将 基本类型变量 转换成字符串。 -
包装类型变量 可以与基本类型变量 进行值比较。
-
系统把一个-128-127之间的整数自动装箱成Integer实例,并放入了一个名为cache的数组中缓存起来。如果以后把一个-128~127之间的整数自动装箱成一个Integer实例时,实际上指向对应的数组元素,因此-128-127之间的同一个整数自动装箱成Integer实例时,永远都是引用cache数组的同一个数组元素,所以它们全部相等;但每次把一个不在-128-127范围内的整数自动装箱成Integer实例时,系统总是重新创建一个Integer实例,所以出现程序中的运行结果。
-
Java7为所有的包装类都提供了一个静态
compare(xxx val1 , xxxval2)
方法,这样开发者就可以通过包装类提供的compare(xxx val1 , xxxval2)
方法来比较两个基本类型值的大小,包括比较两个boolean
类型值。
处理对象
打印对象和toString
方法
toString()
是一个“自我描述”方法。大多会进行方法重写toString
方法。- Object提供的
toString()
方法,,,
== 和 equals方法
-
Java用以测试两个变量是否相等,但对于基本类型 和 引用型变量 会产生不同效果
- 对于基本类型中的数值型变量,产生同样结果
- 对于两个引用类型变量,只有它们指向同一个对象时,==判断才会返回true。==不可用于比较类型上没有父子关系的两个对象。
-
当Java程序直接使用形如“hello”的字符串直接量(包括可以在编译时就计算出来的字符串值)时,JVM将会使用常量池来管理这些字符串;当使用new String(“hello”)时,JVM会先使用常量池来管理"hello"直接量,再调用String 类的构造器来创建一个新的String对象,新创建的Sing对象被保存在堆内存中。换句话说,new String(“hello”)一共产生了两个字符串对象。
-
常量池(constant pool)专门用于管理在编译时被确定并被保存在已编译的.class 文件中的的一些数据。它包括了关于类、方法、接口中的常量,还包括字符串常量。
-
JVM常量池保证相同的字符串直接量只有一个,不会产生多余的副本。
-
使用
new String()
创建的字符串对象是运行时创建出来。它被保存在运行时的内存区(堆内存)中,不会放入常量池中。 -
equals()
是object提供的一个实例方法,因此所有引用变量都可调用该方法来判断是否与其他引用变量相等。但使用这个方法判断两个对象相等的标准与使用==
运算符没有区别,同样要求两个引用变量指向同一个对象才会返回true。因此这个Object 类提供的equals()
方法没有太大的实际意义,如果希望采用自定义的相等标准,则可采用重写equals方法来实现。 -
String已经重写了Object的
equals()
方法,String的equals()
方法判断两个字符串相等的标准是:只要两个字符串所包含的字符序列相同,通过equals()
比较将返回true,否则将返回false。 -
通常而言,正确地重写
equals()
方法应该满足下列条件:- 自反性:对任意x,
x.equals(x)
一定返回true。 - 对称性:对任意x和y,如果
y.equals(x)
返回true,则x.equals(y)
也返回true。 - 传递性:对任意x,y,z,如果
x.equals(y)
返回true,y.equals(z)
返回true,则x.equals(Z)
一定返回true。 - 一致性:对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用
x.equals(y)
多少次,返回的结果应该保持一致,要么一直是true,要么一直是false。 - 对任何不是null的x,
x.equals(null)
一定返回false。
- 自反性:对任意x,
-
Object默认提供的
equals()
只是比较对象的地址,即Object类的equals()
方法比较的结果与一运算符比较的结果完全相同。因此,在实际应用中常常需要重写equals()
方法,重写equals方法时,相等条料是由业务要求决定的,因此equals()
方法的实现也是由业务要求决定的。
类成员
- static修饰的成员为类成员,属于整个类,不属于单个实例。
- 当通过对象来访问类变量时,系统会在底层转换为通过该类来访问类变量。
单例类
- 一个类始终只能创建一个实例
class Singleton
{
// 使用一个类变量来缓存曾经创建的实例
private static Singleton instance;
// 将构造器使用private修饰,隐藏该构造器
private Singleton(){}
// 提供一个静态方法,用于返回Singleton实例
// 该方法可以加入自定义的控制,保证只产生一个Singleton对象
public static Singleton getInstance()
{
// 如果instance为null,表明还不曾创建Singleton对象
// 如果instance不为null,则表明已经创建了Singleton对象,
// 将不会重新创建新的实例
if (instance == null)
{
// 创建一个Singleton对象,并将其缓存起来
instance = new Singleton();
}
return instance;
}
}
public class SingletonTest
{
public static void main(String[] args)
{
// 创建Singleton对象不能通过构造器,
// 只能通过getInstance方法来得到实例
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // 将输出true
}
}
final修饰符
- 用于表示它修饰的类、方法、成员变量 不可变
final成员变量的特点
- final修饰的变量一旦获得初始值,该final变量的值就不能重新赋值。
- final修饰的成员变量必须由程序员显式的指定初始值。
- 如果打算在构造器、初始化块中对成员变量进行初始化,则不要在初始化之前访问final成员变量;由于Java允许通过方法来访问final成员变量,此时将看到系统将final成员变量默认初始化的情况。
final 局部变量
- 局部变量只能显式初始化,因此final修饰的局部变量 可以先声明再初始化赋值,但只能初始化赋值一次。
final修饰的基本类型变量与引用类型变量的区别
- final修饰的基本类型变量不可改变。
- final修饰的引用类型变量保存的引用地址不可改变,但地址所存储的内容可变。
可执行“宏替换”的final变量
- final修饰的变量满足以下条件 可相当于一个直接量(宏变量):
- 使用final修饰符。
- 在定义该final变量时指定了初始值。
- 该初始值在编译时就被确定下来。
- 如果被赋的表达式只是基本的算术表达式或子符串连接运算,没有访问普通变量,调用方法,Java编译器同样会将这种final变量当成“宏变量”处理。
final方法
- final修饰的方法不可被重写。
- 对于一个private方法,因为它仅在当前类中可见,其子类无法访问该方法,所以子类无法重写该方法——如果子类中定义一个与父类private方法有相同方法名、相同形参列表、相同返回值类型的方法,也不是方法重写,只是重新定义了一个新方法。因此,即使使用final修饰一个private访问权限的方法,依然可以在其子类中定义与该方法具有相同方法名、相同形参列表、相同返回值类型的方法。
final类
- final修饰的类不可被继承。
不可变类
- 不可变(immutable)类的意思是创建该类的实例后,该实例的实例变量是不可改变的。Java提供的8个包装类和
java.lang.String
类都是不可变类,当创建它们的实例后,其实例的实例变量不可改变。 - 如果需要创建自定义的不可变类,可遵守如下规则:
- 使用private和final修饰符来修饰该类的成员变量。
- 提供带参数的构造器(或返回该实例的类方法),用于根据传入参数来初始化类里的成员变量。
- 仅为该类的成员变量提供getter方法,不要为该类的成员变量提供setter方法,因为普通方法无法修改final修饰的成员变量。
- 如果有必要,重写Object类的
hashCode()
和equals()
方法。equals()
方法根据关键成员变量来作为两个对象是否相等的标准,除此之外,还应该保证两个用equals()
方法判断为相等的对象的hashCode()
也相等。
public class Address
{
private final String detail;
private final String postCode;
// 在构造器里初始化两个实例变量
public Address(String detail, String postCode)
{
this.detail = detail;
this.postCode = postCode;
}
// 仅为两个实例变量提供getter方法
public String getDetail()
{
return this.detail;
}
public String getPostCode()
{
return this.postCode;
}
//重写equals()方法,判断两个对象是否相等。
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj != null && obj.getClass() == Address.class)
{
var ad = (Address) obj;
// 当detail和postCode相等时,可认为两个Address对象相等。
if (this.getDetail().equals(ad.getDetail())
&& this.getPostCode().equals(ad.getPostCode()))
{
return true;
}
}
return false;
}
public int hashCode()
{
return detail.hashCode() + postCode.hashCode() * 31;
}
}
缓存实例的不可变类
class CacheImmutale
{
private static int MAX_SIZE = 10;
// 使用数组来缓存已有的实例
private static CacheImmutale[] cache
= new CacheImmutale[MAX_SIZE];
// 记录缓存实例在缓存中的位置,cache[pos-1]是最新缓存的实例
private static int pos = 0;
private final String name;
private CacheImmutale(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
public static CacheImmutale valueOf(String name)
{
// 遍历已缓存的对象,
for (var i = 0; i < MAX_SIZE; i++)
{
// 如果已有相同实例,直接返回该缓存的实例
if (cache[i] != null
&& cache[i].getName().equals(name))
{
return cache[i];
}
}
// 如果缓存池已满
if (pos == MAX_SIZE)
{
// 把缓存的第一个对象覆盖,即把刚刚生成的对象放在缓存池的最开始位置。
cache[0] = new CacheImmutale(name);
// 把pos设为1
pos = 1;
}
else
{
// 把新创建的对象缓存起来,pos加1
cache[pos++] = new CacheImmutale(name);
}
return cache[pos - 1];
}
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj != null && obj.getClass() == CacheImmutale.class)
{
var ci = (CacheImmutale) obj;
return name.equals(ci.getName());
}
return false;
}
public int hashCode()
{
return name.hashCode();
}
}
public class CacheImmutaleTest
{
public static void main(String[] args)
{
var c1 = CacheImmutale.valueOf("hello");
var c2 = CacheImmutale.valueOf("hello");
// 下面代码将输出true
System.out.println(c1 == c2);
}
}
- 由于通过new构造器创建Integer对象不会启用缓存,因此性能较差,Java9已经将该构造器标记为过时。
抽象类
抽象方法和抽象类
-
抽象方法和抽象类必须使用abstract修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法。
-
抽象方法和抽象类的规则如下:
- 抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。
- 抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。即使抽象类里不包含抽象方法,这个抽象类也不能创建实例。
- 抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。
- 含有抽象方法的类(包括直接定义了一个抽象方法;或继承了一个抽象父类,但没有完全实现父类包含的抽象方法;或实现了一个接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义成抽象类。
-
抽象方法
[修饰符] abstract 返回值类型 方法名 () ;
-
当使用abstract 修饰类时,表明这个类只能被继承;
当使用abstract 修饰方法时,表明这个方法必须由子类提供实现(即重写)。
-
而final修饰的类不能被继承,final修饰的方法不能被重写。因此final和abstract永远不能同时使用。
-
除此之外,当使用 static修饰一个方法时,表明这个方法属于该类本身,即通过类就可调用该方法,但如果该方法被定义成抽象方法,则将导致通过该类来调用该方法时出现错误(调用了一个没有方法体的方法肯定会引起错误)。因此static和 abstract不能同时修饰某个方法,即没有所谓的类抽象方法。
-
static和abstract并不是绝对互斥的,static和abstract虽然不能同时修饰某个方法,但它们可以同时修饰内部类。
-
abstract 关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此abstract方法不能定义为private 访问权限,即private和abstract不能同时修饰方法。
父类的作用
- 模板模式
Java9 改进的接口
- Java9对接口进行了改进,允许在接口中定义默认方法和类方法默认方法和类方法都可以提供方法实现,Java9为接口增加了一种私有方法,私有方法也可提供方法实现。
接口的概念
- 接口 定义一批类所遵循的规范,只规定这批类里必须提供的方法,不提供任何实现。
Java9中接口的定义
[修饰符] interface 接口名 extends 父接口1,父接口2...
{
常量定义;
抽象方法定义;
内部类、接口、枚举定义;
私有方法、默认方法或类方法定义;
}
-
对上面语法的详细说明如下:
- 修饰符可以是pubic或者省略,如果省略了pubic访问控制符,则默认采用包权限访问控制符,即只有在相同包结构下才可以访问该接口。
- 接口名应与类名采用相同的命名规则,即如果仅从语法角度来看,接口名只要是合法的标识符即可;如果要遵守Java可读性规范,则接口名应由多个有意义的单词连缀而成,每个单词首空母大写,单词与单词之间无须任何分隔符。接口名通常能够使用形容词。
- 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
-
由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。
-
对比接口和类的定义方式,不难发现接口的成员比类里的成员少了两种,而且接口里的成员变量只能是静态常量,接口里的方法只能是抽象方法、类方法、默认方法或私有方法。
-
前面已经说过了,接口里定义的是多个类共同的公共行为规范,因此**接口里的常量、方法、内部类和内部枚举都是public访问权限。**定义接口成员时,可以省略访问控制修饰符,如果指定访问控制修伤符,则只能使用public访问控制修饰符。
-
Java9为接口增加了一种新的私有方法,其实私有方法的主要作用就是作为工具方法,为接口中的默认方法或类方法提供支持。私有方法可以拥有方法体,但不能使用default修饰。私有方法可以使用static修饰。
-
对于接口里定义的静态常量而言,它们是接口相关的,因此系统会自动为这些成员变量增加
static final
修饰符。也就是说,在接口中定义成员变量时,不管是否使用public static final修饰符,接口里的成员变量总是使用这三个修饰符来修饰。而且接口里没有构造器和初始化块,因此接口里定义的成员变量只能在定义时指定默认值。 -
接口里定义的方法只能是抽象方法、类方法、默认方法或私有方法,因此如果不是定义默认方法、类方法或私有方法,系统将自动为普通方法增加abstract修饰符;定义接口里的普通方法时不管是否使用
public abstract
修饰符,接口里的普通方法总是使用pubic abstract
来修饰。 -
接日里的普通方法不能有方法实现(方法体);但类方法、默认方法、私有方法都必须有方法实现(方法体)。
-
接口里定义的 内部类、内部接口、内部枚举默认都采用
public static
两个修饰符,不管定义时是否指定这两个修饰符,系统都回使用 public static 修饰。
public interface Output
{
// 接口里定义的成员变量只能是常量
//省略了 public static final 修饰符
int MAX_CACHE_LINE = 50;
// 接口里定义的普通方法只能是public的抽象方法
//省略了public abstract 修饰符
void out();
void getData(String msg);
// 在接口中定义默认方法,需要使用default修饰
default void print(String... msgs)
{
for (var msg : msgs)
{
System.out.println(msg);
}
}
// 在接口中定义默认方法,需要使用default修饰
//省略了public 修饰符
default void test()
{
System.out.println("默认的test()方法");
}
// 在接口中定义类方法,需要使用static修饰
//省略了public 修饰符
static String staticTest()
{
return "接口里的类方法";
}
// 定义私有方法
private void foo()
{
System.out.println("foo私有方法");
}
// 定义私有静态方法
private static void bar()
{
System.out.println("bar私有静态方法");
}
}
-
从Java8开始,在接口里允许定义默认方法,默认方法必须使用default修饰,该方法不能使用static修饰,无论程序是否指定,默认方法总是使用 public 修饰——如果开发者没有指定public,系统会自动为默认方法添加public修饰符。由于默认方法并没有static修饰,因此不能直接使用接口来调用默认方法,需要使用接口的实现类的实例来调用这些默认方法。
-
接口的默认方法其实就是实例方法,但由于早期Java的设计是:接口中的实例方法不能有方法体;Java8也不能直接“推倒”以前的规则,因此只好重定义一个所谓的“默认方法”,默认方法就是有方法体的实例方法。
-
无论程序是否指定,类方法总是使用public 修饰。
-
从某个角度来看,接口可被当成一个特殊的类,因此一个Java源文件里最多只能有一个public接口,如果一个Java源文件里定义了一个public 接口,则该源文件的主文件名必须与该接口名相同。
接口的继承
- 和类继承相似,子接口扩展某个父接口,将会获得父接口里定义的所有抽象方法、常量。
使用接口
- 接口不能用于创建实例,但接口可以用于声明引用类型变量。当使用接口来声明引用类型变量时,这个引用类型变量必须引用到其实现类的对象。除此之外,接口的主要用途就是被实现类实现。归纳起条,接口主要有如下用途:
- 定义变量,也可用于进行强制类型转换。
- 调用接口中定义的常量。
- 被其他类实现。
- 一个类可以实现多个接口
[修饰符] class 类名 extends 父类 implements 接口1,接口2
{
类体部分;
}
- 实现接口与继承父类相似,一样可以获得接口里的 常量(成员变量)、方法(抽象方法、和默认方法)。
- 一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);否则,该类将保留从父接口那里继承到的抽象方法,该类也必须被定义成抽象类。
- 实现接口方法时,必须使用public访问控制修饰符,因为接口里的方法都是public的,而子类(相当于实现类)重写父类方法时访问权限只能更大或者相等,所以实现类实现接口里的方法时只能使用public 访问权限。
- 接口不能显式继承任何类,但所有接口类型的引用变量都可以直接赋给Object类型引用变量,类似于多态,从而可以调用Object类中实现的抽象方法 和接口中定义的默认方法。
接口和抽象类
-
接口和抽象类的共同点:
- 都不能被实例化。
- 都可以包含抽象方法。
-
设计上的差别:
- 接口体现一种规范。
- 抽象类体现一种模板式设计。
-
用法上差别:
-
接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;
抽象类则完全可以包含普通方法。
-
接口里只能定义静态常量,不能定义普通成员变量;
抽象类里则既可以定义普通成员变量,也可以定义静态常量。
-
接口里不包含构造器;
抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
-
接口里不能包含初始化块;
但抽象类则完全可以包含初始化块。
-
一个类最多只能有一个直接父类,包括抽象类;
但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。
-
面向接口编程
- 简单工厂模式
public interface Output
{
// 接口里定义的成员变量只能是常量
int MAX_CACHE_LINE = 50;
// 接口里定义的普通方法只能是public的抽象方法
void out();
void getData(String msg);
// 在接口中定义默认方法,需要使用default修饰
default void print(String... msgs)
{
for (var msg : msgs)
{
System.out.println(msg);
}
}
// 在接口中定义默认方法,需要使用default修饰
default void test()
{
System.out.println("默认的test()方法");
}
// 在接口中定义类方法,需要使用static修饰
static String staticTest()
{
return "接口里的类方法";
}
// 定义私有方法
private void foo()
{
System.out.println("foo私有方法");
}
// 定义私有静态方法
private static void bar()
{
System.out.println("bar私有静态方法");
}
}
public class Computer
{
private Output out;
public Computer(Output out)
{
this.out = out;
}
// 定义一个模拟获取字符串输入的方法
public void keyIn(String msg)
{
out.getData(msg);
}
// 定义一个模拟打印的方法
public void print()
{
out.out();
}
}
public class BetterPrinter implements Output
{
private String[] printData = new String[MAX_CACHE_LINE * 2];
// 用以记录当前需打印的作业数
private int dataNum = 0;
public void out()
{
// 只要还有作业,继续打印
while (dataNum > 0)
{
System.out.println("高速打印机正在打印:" + printData[0]);
// 把作业队列整体前移一位,并将剩下的作业数减1
System.arraycopy(printData, 1, printData, 0, --dataNum);
}
}
public void getData(String msg)
{
if (dataNum >= MAX_CACHE_LINE * 2)
{
System.out.println("输出队列已满,添加失败");
}
else
{
// 把打印数据添加到队列里,已保存数据的数量加1。
printData[dataNum++] = msg;
}
}
}
public class OutputFactory
{
public Output getOutput()
{
//return new Printer();
//获取打印机实例
return new BetterPrinter();
}
public static void main(String[] args)
{
//产生打印工厂实例
var of = new OutputFactory();
//产生字符串 输入 输出 终端实例
var c = new Computer(of.getOutput());
c.keyIn("轻量级Java EE企业应用实战");
c.keyIn("疯狂Java讲义");
c.print();
}
}
BetterPrinter
类按照Output规范 可替换 而不影响其它代码实现。
- 命令模式
public interface Command
{
// 接口里定义的process()方法用于封装“处理行为”
void process(int element);
}
public class PrintCommand implements Command
{
public void process(int element)
{
System.out.println("迭代输出目标数组的元素:" + element);
}
}
public class SquareCommand implements Command
{
public void process(int element)
{
System.out.println("数组元素的平方是:" + element * element);
}
}
public class ProcessArray
{
//封装处理行为
public void process(int[] target, Command cmd)
{
for (var t : target)
{
cmd.process(t);
}
}
}
public class CommandTest
{
public static void main(String[] args)
{
var pa = new ProcessArray();
int[] target = {3, -4, 6, 4};
// 第一次处理数组,具体处理行为取决于PrintCommand
pa.process(target, new PrintCommand());
System.out.println("------------------");
// 第二次处理数组,具体处理行为取决于SquareCommand
pa.process(target, new SquareCommand());
}
}
内部类
- 定义在其它类内部的类
- 作用:
- 内部类提供了要好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
- 内部类成员可以直接访间外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访间。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
- 匿名内部类适合用于创建那些仅需要一次使用的类。
- 内部类与外部类的区别:
- 内部类比外部类可以多使用三个修饰符:private、protected、static—一外部类不可以使用这三个修饰符。
- 非静态内部类不能拥有静态成员。
非静态内部类
-
外部类的上一级程序单元是包,所以有两个作用域,只需两种权限;
内部类的上一级程序单元是外部类,具有四个作用域,有四个访问权限。
public class Cow
{
private double weight;
// 外部类的两个重载的构造器
public Cow(){}
public Cow(double weight)
{
this.weight = weight;
}
// 定义一个非静态内部类
private class CowLeg
{
// 非静态内部类的两个实例变量
private double length;
private String color;
// 非静态内部类的两个重载的构造器
public CowLeg(){}
public CowLeg(double length, String color)
{
this.length = length;
this.color = color;
}
// 下面省略length、color的setter和getter方法
public void setLength(double length)
{
this.length = length;
}
public double getLength()
{
return this.length;
}
public void setColor(String color)
{
this.color = color;
}
public String getColor()
{
return this.color;
}
// 非静态内部类的实例方法
public void info()
{
System.out.println("当前牛腿颜色是:"
+ color + ", 高:" + length);
// 直接访问外部类的private修饰的成员变量
System.out.println("本牛腿所在奶牛重:" + weight); // ①
}
}
//创建内部类实例 的方法
public void test()
{
var cl = new CowLeg(1.12, "黑白相间");
cl.info();
}
public static void main(String[] args)
{
var cow = new Cow(378.9);
cow.test();
}
}
-
编译上面程序,看到在文件所在路径生成了两个class文件,一个是
Cow.class
,另一个是Cow$CowLeg.class
,前者是外部类Cow
的class文件,后者是内部类CowLeg
的class文件,即成员内部类(包括静态内部类、非静态内部类)的class文件总是这种形式:OuterClass$ImnerClass.class
。 -
非静态内部类对象必须寄生在外部类对象里,而外部类对象则不必一定有非静态内部类对象寄生其中。简单地说,如果存在一个非静态向部类对象,则一定存在一个被它寄生的外部类对象。但外部类对象存在时,外部类对象里不一定寄生了非静态向都类对象。因此外部类对象访问非静态内部类成员时,可能非静态普通内部类对象根本不存在!而非静态内部类对象访间外部类成员时,外部类对象一定存在。
-
根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例等。总之,不允许在外都类的静态成员中直接使用非静态内部类。
静态内部类
- 静态内部类可以包含静态成员,也可包含非静态成员。
- 静态内部类只能访问外部类的类成员。
- 外部类可以通过静态内部类的类名作为调用者来访问静态内部类的类成员。
- 在接口只能定义静态内部类,默认用 public static 修饰。
使用内部类
- 内部类对象只能寄生在外部类的对象里。
局部内部类
- 局部内部类不能使用访问控制符 和 static 修饰。
匿名内部类
- 创建只需要使用一次的类。
new 实现接口() |父类构造器(实参列表)
{
//匿名内部类类体部分
}
- 匿名内部类必须继承一个父类,或实现一个接口,最只能继承一个父类或实现一个接口。
- 匿名内部类不能是抽象类,系统在创建匿名内部类时,会立即创建匿名内部类对象。
- 匿名内部类没有构造器,但可以定义初始化块。
- 当创建匿名内部类时,必须实现所有的抽象方法。
- java8以后版本的JDK将这个功能称为“effectively final”,它的意思是对于被匿名内部类访问的局部变量,可以用final修饰,也可以不用final修饰,但必须按照有final修饰的方式来用——也就是一次赋值后,以后不能重新赋值。
Java11增强的Lambda表达式
- Lambda表达式支持将代码块作为方法参数,Lambda表达式允许使用更简洁的代码来创建只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。
Lambda表达式入门
public interface Command
{
// 接口里定义的process()方法用于封装“处理行为”
void process(int element);
}
public class CommandTest2
{
public static void main(String[] args)
{
var pa = new ProcessArray();
int[] array = {3, -4, 6, 4};
// 处理数组,具体处理行为取决于匿名内部类
pa.process(array, (int element)->{
System.out.println("数组元素的平方是:" + element * element);
});
}
}
public class CommandTest
{
public static void main(String[] args)
{
var pa = new ProcessArray();
int[] target = {3, -4, 6, 4};
// 处理数组,具体处理行为取决于匿名内部类
pa.process(target, new Command()
{
public void process(int element)
{
System.out.println("数组元素的平方是:" + element * element);
}
});
}
}
- Lambda表达式的主要作用就是代替匿名内部类的烦琐语法。它由三部分组成:
- 形参列表。形参列表允许省略形参类型。如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略。
- 箭头( ->)。必须通过英文中画线和大于符号组成。
- 代码块。如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号,那么这条语句就不要用花括号表示语句结束。Lambda代码块只有一条return语句,甚至可以省略return关键字。Lambda 表达式需要返回值,而它的代码块仅有一条省略的return语句,Lambda表达式会自动返回这条语句的值。
Lambda表达式与函数式接口
-
Lambda表达式的类型,也被称为“目标类型(target type)”,Lambda表达式的目标类型必须是“函教式接口(functional interface)”。
-
函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。
-
如果采用匿名内部类语法来创建函数式接口的实例,则只需要实现一个抽象方法,在这种情况下即可采用Lambda表达式来创建对象,该表达式创建出来的对象的目标类型就是这个函数式接口。
-
Java8专门为函数式接口提供了
@FunctionalInterface
注解,该注解通常放在接口定义前面,该注解对程序功能没有任何作用,它用于告诉编译器执行更严格检查——检查该接口必须是函数式接口,否则编译器就会报错。 -
从上面粗体字代码可以看出,Lambda表达式实现的是匿名方法——因此它只能实现特定函数式接口中的唯一方法。这意味着Lambda表达式有如下两个限制:
- Lambda表达式的目标类型必须是明确的函数式接口。
- Lambda表达式只能为函数式接口创建对象。Lambda表达式只能实现一个方法,因此它只能为只有一个抽象方法的接口(函数式接口)创建对象。
-
了保证Lambda表达式的目标类型是一个明确的函数式接口,可以有如下三种常见方式:
- 将Lambda表达式赋值给函数式接口类型的变量。
- 将Lambda表达式作为函数式接口类型的参数传给某个方法。
- 使用函数式接口对Lambda表达式进行强制类型转换。
在Lambda表达式中使用var
- 使用Lambda表达式对
var
定义的变量赋值时,必须明确定义Lambda表达式的目标类型。
@interface NotNull{}
interface Predator
{
void prey(@NotNull String animal);
}
public class VarInLambda
{
public static void main(String[] args)
{
// 使用Lambda表达式对var变量赋值
// 必须显式指定Lambda表达式的目标类型
var run = (Runnable)() -> {
for (var i = 0; i < 100; i++)
{
System.out.println();
}
};
// 使用var声明Lambda表达式的形参类型,
// 这样即可为Lambda表达式的形参添加@NotNull注解
Predator predator = (@NotNull var animal) -> {
System.out.println("老鹰正在猎捕" + animal);
};
predator.prey("兔子");
}
}
方法引用与构造器引用
种类 | 示例 | 说明 | 对应的Lambda表达式 |
---|---|---|---|
引用类方法 | 类名::类方法 | 函数式接口中被实现方法的全部参数传给该类方法作为参数 | (a,b,…)->类名.类方法(a,b,…) |
引用某类对象的实例方法 | 特定对象::实例方法 | 函数式接口中被实现方法的全部参数传给该方法作为参数 | (a,b,…)->特定对象.实例方法(a,b,…) |
引用某类对象的实例方法 | 类名::实例方法 | 函数式接口中被实现方法的第一个参数作为调用者,后面的参数全部传给该方法作为参数 | (a,b,…)->a.实例方法(b,…) |
引用构造器 | 类名::new | 函数式接口中被实现方法的全部参数传给该构造器作为参数 | (a,b,…)->new 类名(a,b,…) |
@FunctionalInterface
interface Converter{
Integer convert(String from);
}
@FunctionalInterface
interface MyTest
{
String test(String a, int b, int c);
}
@FunctionalInterface
interface YourTest
{
JFrame win(String title);
}
public class MethodRefer
{
public static void main(String[] args)
{
// 下面代码使用Lambda表达式创建Converter对象
// Converter converter1 = from -> Integer.valueOf(from);
// 方法引用代替Lambda表达式:引用类方法。
// 函数式接口中被实现方法的全部参数传给该类方法作为参数。
// Converter converter1 = Integer::valueOf;
// Integer val = converter1.convert("99");
// System.out.println(val); // 输出整数99
// 下面代码使用Lambda表达式创建Converter对象
// Converter converter2 = from -> "fkit.org".indexOf(from);
// 方法引用代替Lambda表达式:引用特定对象的实例方法。
// 函数式接口中被实现方法的全部参数传给该方法作为参数。
// Converter converter2 = "fkit.org"::indexOf;
// Integer value = converter2.convert("it");
// System.out.println(value); // 输出2
// 下面代码使用Lambda表达式创建MyTest对象
// MyTest mt = (a, b, c) -> a.substring(b, c);
// 方法引用代替Lambda表达式:引用某类对象的实例方法。
// 函数式接口中被实现方法的第一个参数作为调用者,
// 后面的参数全部传给该方法作为参数。
// MyTest mt = String::substring;
// String str = mt.test("Java I Love you", 2, 9);
// System.out.println(str); // 输出:va I Lo
// 下面代码使用Lambda表达式创建YourTest对象
// YourTest yt = a -> new JFrame(a);
// 构造器引用代替Lambda表达式。
// 函数式接口中被实现方法的全部参数传给该构造器作为参数。
YourTest yt = JFrame::new;
JFrame jf = yt.win("我的窗口");
System.out.println(jf);
}
}
Lambda表达式与匿名内部类的联系和区别
-
Lambda表达式与匿名内部类存在如下相同点:
- Lambda 表达式与匿名内部类一样,都可以直接访问“effectively final”的局部变量,以及外部类的成员变量(包括实例变量和类变量)。
- Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。
-
区别:
-
匿名内部类可以为任意接口创建实例——不管接口包含多少个抽象方法,只要匿名内部类实项所有的抽象方法即可;
但Lambda表达式只能为函数式接口创建实例。
-
匿名内部类可以为抽象类甚至普通类创建实例;
但Lambda表达式只能为函数式接口创建实例。
-
匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;
但Lambda表达式的代码块不允许调用接口中定义的默认方法,创建实例后才能调用。
-
使用Lambda方法调用Arrays的类方法
枚举类
- 实例有限且固定的类 称为枚举类
枚举类入门
-
Java 5新增了一个
enum
关键字(它与class
、interface
关键字的地位相同),用以定义枚举类。枚举类是一种特殊的类,它一样可以有自己的成员变量、方法,可以实现一个或者多个接口,也可以定义自己的构造器。 -
一个Java源文件中最多只能定义一个public访问权限的枚举类,且该Java源文件也必须和该枚举类的类名相同。
-
与普通类有如下简单区别:
-
枚举类可以实现一个或多个接口,使用
enum
定义的枚举类默认继承了java.lang.Enum
类,而不是默认继承Object类,因此枚举类不能显式继承其他父类。其中
java.lang.Enum
类实现了java.lang.Serializable
和java.lang.Comparable
两个接口。 -
使用
enum
定义、非抽象的枚举类默认会使用final修饰;只要枚举类包含抽象方法,它就是一个抽象枚举类,系统默认使用
abstract
修饰。 -
枚举类的构造器只能使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰;
如果强制指定访问控制符,则只能指定private修饰符。由于枚举类的所有构造器都是private的,而子类构造器总要调用父类构造器一次,因此枚举类不能派生子类。
-
枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加
public static final
修饰,无须程序员显式添加。
-
-
枚举类默认提供了一个
values()
方法,该方法可以方便的遍历所有枚举值。 -
switch的控制表达式可以是任何枚举类型。
枚举类的成员变量、方法和构造器
public enum Gender
{
MALE, FEMALE;
private String name;
public void setName(String name)
{
switch (this)
{
case MALE:
if (name.equals("男"))
{
this.name = name;
}
else
{
System.out.println("参数错误");
return;
}
break;
case FEMALE:
if (name.equals("女"))
{
this.name = name;
}
else
{
System.out.println("参数错误");
return;
}
break;
}
}
public String getName()
{
return this.name;
}
}
- 通常枚举类设计成不可变类,成员变量都使用
private final
修饰。
public enum Gender
{
// 此处的枚举值必须调用对应构造器来创建
MALE("男"), FEMALE("女");
private final String name;
// 枚举类的构造器只能使用private修饰
private Gender(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
}
public class GenderTest
{
public static void main(String[] args)
{
//通过valueOf()方法获取指定枚举类的枚举值
Gender g = Gender.valueOf("FEMALE");
g.setName("女");
System.out.println(g + "代表:" + g.getName());
// 此时设置name值时将会提示参数错误。
g.setName("男");
System.out.println(g + "代表:" + g.getName());
}
}
实现接口的枚举类
- 枚举类实现接口与普通类一样需要实现该接口所包含的普通方法。
- 不同枚举值可以用不同方法实现
public interface GenderDesc
{
void info();
}
public enum Gender implements GenderDesc
{
// // 此处的枚举值必须调用对应构造器来创建
// MALE("男"), FEMALE("女");
// 此处的枚举值必须调用对应构造器来创建
MALE("男")
// 花括号部分实际上是一个类体部分
//相当于创建Gender的匿名子类的实例
//由于继承了接口,包含了抽象方法,所以系统自动为该枚举类添加 abstract 关键字
{
public void info()
{
System.out.println("这个枚举值代表男性");
}
},
FEMALE("女")
{
public void info()
{
System.out.println("这个枚举值代表女性");
}
};
// 其他部分与codes\06\6.9\best\Gender.java中的Gender类完全相同
private final String name;
// 枚举类的构造器只能使用private修饰
private Gender(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// // 增加下面的info()方法,实现GenderDesc接口必须实现的方法
// public void info()
// {
// System.out.println(
// "这是一个用于用于定义性别的枚举类");
// }
}
包含抽象方法的枚举类
public enum Operation
{
PLUS
{
public double eval(double x, double y)
{
return x + y;
}
},
MINUS
{
public double eval(double x, double y)
{
return x - y;
}
},
TIMES
{
public double eval(double x, double y)
{
return x * y;
}
},
DIVIDE
{
public double eval(double x, double y)
{
return x / y;
}
};
// 为枚举类定义一个抽象方法eval()
// 这个抽象方法由不同的枚举值提供不同的实现
public abstract double eval(double x, double y);
public static void main(String[] args)
{
System.out.println(Operation.PLUS.eval(3, 4));
System.out.println(Operation.MINUS.eval(5, 4));
System.out.println(Operation.TIMES.eval(5, 4));
System.out.println(Operation.DIVIDE.eval(5, 4));
}
}
- 编译上面程序会生成5个class文件,其实Operation对应一个class文件,它的4个匿名内部子类分别各对应一个class文件。
- 枚举类里定义抽象方法时不能使用
abstract
关键字将枚举类定义成抽象类(因为系统自动会为它添加abstract
关键字),但因为枚举类需要显式创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误。
对象与垃圾回收
- 垃圾回收机制具有如下特征:
- 垃圾回收机制只负责回收堆内存中的对象,不会回收任何物理资源(例如数据库连接、网络IO等资源)。
- 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候进行。当对象永久性地失去引用后,系统就会在合适的时候回收它所占的内存。
- 在垃圾回收机制回收任何对象之前,总会先调用它的
finalize()
方法,该方法可能使该对象重新复活(让一个引用变量重新引用该对象),从而导致垃圾回收机制取消回收。
对象在内存中的状态
强制垃圾回收
- “强制”(通知)系统进行垃圾回收:
- 调用
System
类的gc()
静态方法:System.gc()
- 调用
Runtime
对象的gc()
实例方法:Runtime.getRuntime().gc()
- 调用
finalize方法
-
Java提供了默认机制来清理该对象的资源,这个机制就是
finalize()
方法。 -
任何Java类都可以重写Object类的
finalize()
方法,在该方法中清理该对象占用的资源。如果程序终止之前始终没有进行垃圾回收,则不会调用失去引用对象的finalize()
方法来清理资源。垃圾回收机制何时调用对象的finalize()
方法是完全透明的,只有当程序认为需要更多的额外内存时,垃圾回收机制才会进行垃圾回收。因此,完全有可能出现这样一种情形:某个失去引用的对象只占用了少量内存,而且系统没有产生严重的内存需求,因此垃圾回收机制并没有试图回收该对象所占用的资源,所以该对象的finalize()
方法也不会得到调用。 -
finalize0方法具有如下4个特点:
-
永远不要主动调用某个对象的
finalize()
方法,该方法应交给垃圾回收机制调用。 -
finalize()
方法何时被调用,是否被调用具有不确定性,不要把finalize()
方法当成一定会被执行的方法。 -
当JVM执行可恢复对象的
finalize()
方法时,可能使该对象或系统中其他对象重新变成可达状态。 -
当JVM执行
finalize()
方法时出现异常时,垃圾回收机制不会报告异常,程序继续执行。
-
对象的软、弱、虚引用
-
强引用(StrongReference)
这是Java程序中最常见的引用方式。程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象,前面介绍的对象和数组都采用了这种强引用的方式。当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收。
-
软引用(SoftReference)
软引用需要通过SoftReference类来实现,当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象;当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中。 -
弱引用(WeakReference)
弱引用通过WeakReference类实现,弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。当然,并不是说当一个对象只有弱引用时,它就会立即被回收——正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。 -
虚引用(PhantomReference)
虚引用是通过PhantomReference 类实现,虚引用完全类似于没有引用。虚引用对对象本身没有太大的影响,对象甚至感觉不到虚引用的存在。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列(ReferenceQueue)联合使用。
-
引用队列由
java.lang.ref.ReferenceQueue
类表示,它用于保存被回收后对象的引用。当联合使用软引用、弱引用和引用队列时,系统在回收被引用的对象之后,将把被回收对象对应的引用添加到关联的引用队列中。与软引用和弱引用不同的是,虚引用在对象被释放之前,将把它对应的虚引用添加到它关联的引用队列中,这使得可以在对象被回收之前采取行动。 -
软引用和弱引用可以单独使用,但虚引用不能单独使用,单独使用虚引用没有太大的意义。虚引用的主要作用就是跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否已经包公了该虚引用,从而了解虚引用所引用的对象是否即将被回收。
public class ReferenceTest
{
public static void main(String[] args)
throws Exception
{
// 创建一个字符串对象
var str = new String("疯狂Java讲义");
// 创建一个弱引用,让此弱引用引用到"疯狂Java讲义"字符串
var wr = new WeakReference(str); // ①
// 切断str引用和"疯狂Java讲义"字符串之间的引用
str = null; // ②
// 取出弱引用所引用的对象
System.out.println(wr.get()); // ③
// 强制垃圾回收
System.gc();
System.runFinalization();
// 再次取出弱引用所引用的对象
System.out.println(wr.get()); // ④
}
}
修饰符的适用范围
- 修饰符:
- 四个基本访问控制符
- abstract
- final
- static
- default (接口中的默认方法 )
- strictfp (精确浮点)
- synchronized
- native (以 C 语言实现的抽象方法)
- trensient
- volatile
多版本的JAR包
- 当开发了一个应用程序后,这个应用程序包含了很多类,如果需要把这个应用程序提供给别人使通常会将这些类文件打包成一个JAR文件,把这个JAR文件提供给别人使用。只要别人在系统的CLASSPATEH 环境变量中添加这个JAR文件,则lava虚拟机就可以自动在内存中解压这个JAR包,把这个JAR文件当成一个路径,在这个路径中查找所需要的类或包层次对应的路径结构。
- 实际上JAR就是一个路径。
jar命令详解
用法: jar [OPTION...] [ [--release VERSION] [-C dir] files] ...
jar 创建类和资源的档案, 并且可以处理档案中的
单个类或资源或者从档案中还原单个类或资源。
示例:
# 创建包含两个类文件的名为 classes.jar 的档案:
jar --create --file classes.jar Foo.class Bar.class
# 使用现有的清单创建档案, 其中包含 foo/ 中的所有文件:
jar --create --file classes.jar --manifest mymanifest -C foo/ .
# 创建模块化 jar 档案, 其中模块描述符位于
# classes/module-info.class:
jar --create --file foo.jar --main-class com.foo.Main --module-version 1.0
-C foo/ classes resources
# 将现有的非模块化 jar 更新为模块化 jar:
jar --update --file foo.jar --main-class com.foo.Main --module-version 1.0
-C foo/ module-info.class
# 创建包含多个发行版的 jar, 并将一些文件放在 META-INF/versions/9 目录中:
jar --create --file mr.jar -C foo classes --release 9 -C foo9 classes
要缩短或简化 jar 命令, 可以在单独的文本文件中指定参数,
并使用 @ 符号作为前缀将此文件传递给 jar 命令。
示例:
# 从文件 classes.list 读取附加选项和类文件列表
jar --create --file my.jar @classes.list
主操作模式:
-c, --create 创建档案
-i, --generate-index=FILE 为指定的 jar 档案生成
索引信息
-t, --list 列出档案的目录
-u, --update 更新现有 jar 档案
-x, --extract 从档案中提取指定的 (或全部) 文件
-d, --describe-module 输出模块描述符或自动模块名称
在任意模式下有效的操作修饰符:
-C DIR 更改为指定的目录并包含
以下文件
-f, --file=FILE 档案文件名。省略时, 基于操作
使用 stdin 或 stdout
--release VERSION 将下面的所有文件都放在
jar 的版本化目录中 (即 META-INF/versions/VERSION/)
-v, --verbose 在标准输出中生成详细输出
在创建和更新模式下有效的操作修饰符:
-e, --main-class=CLASSNAME 捆绑到模块化或可执行
jar 档案的独立应用程序
的应用程序入口点
-m, --manifest=FILE 包含指定清单文件中的
清单信息
-M, --no-manifest 不为条目创建清单文件
--module-version=VERSION 创建模块化 jar 或更新
非模块化 jar 时的模块版本
--hash-modules=PATTERN 计算和记录模块的散列,
这些模块按指定模式匹配并直接或
间接依赖于所创建的模块化 jar 或
所更新的非模块化 jar
-p, --module-path 模块被依赖对象的位置, 用于生成
散列
只在创建, 更新和生成索引模式下有效的操作修饰符:
-0, --no-compress 仅存储; 不使用 ZIP 压缩
其他选项:
-?, -h, --help[:compat] 提供此帮助,也可以选择性地提供兼容性帮助
--help-extra 提供额外选项的帮助
--version 输出程序版本
如果模块描述符 'module-info.class' 位于指定目录的
根目录中, 或者位于 jar 档案本身的根目录中, 则
该档案是一个模块化 jar。以下操作只在创建模块化 jar,
或更新现有的非模块化 jar 时有效: '--module-version',
'--hash-modules' 和 '--module-path'。
如果为长选项提供了必需参数或可选参数, 则它们对于
任何对应的短选项也是必需或可选的。
-
创建jar文件:
jar cf test.jar -C dist/
-
创建jar文件并显示压缩过程:
jar cvf test.jar -C dist/
-
不使用清单文件:
jar cvfM test.jar -C dist/
-
自定义清单文件内容:
jar cvfm test.jar manifest.mf -C dist/
-
查看jar包内容:
jar tf test.jar
-
查看jar包的详细内容:
jar tvf test.jar
-
解压缩:
jar xf test.jar
-
带提示信息解压缩:
jar xvf test.jar
-
更新jar文件:
jar uf test.jar Hello.class
-
更新jar文件并显示详细信息:
jar uvf test.jar Hello.class
-
创建多版本jar包:
jar cvf test.jar -C dist7/.--release 9 -C dist/.
创建可执行JAR包
-
创建可执行JAR包的关键在于:
让javaw命令知道JAR包中哪个类是主类,javaw命令可通过运行该主类运行程序。
-
JAR有一个 -e 选项 用于指定JAR包作为程序入口的主类名。如:
jar cvfe test.jar test.Test test
-
运行JAR包:
-
使用java命令
java -jar test.jar
-
使用javaw命令
javaw test.jar
-
常见的压缩工具的使用
- WinRAR、WinZip,,,