5.1 包装类
- Java的8种数据类型对应包装类(Wrapper classes):Integer,Long,Float,Double,Short,Byte,Character, Boolean。
- The wrapper classes are immutable.
- The wrapper classes are final, so you cannot subclass them.
- 基本类型变量和包装类对象之间的转换-自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)功能。
- 例:AutoBoxingUnboxing.java
public class AutoBoxingUnboxing {
public static void main(String[] args) {
// 直接把一个基本类型变量赋给Integer对象
Integer inObj = 5;
// 直接把一个boolean类型变量赋给一个Object类型的变量
Object boolObj = true;
// 直接把一个Integer对象赋给int类型的对象
int it = inObj;
if (boolObj instanceof Boolean)
{
// 先把Object对象强制类型转换为Boolean类型,再赋给boolean变量
boolean b = (Boolean)boolObj;
System.out.println(b);
}
}
}
- 包装类还可以实现基本类型变量和String对象的转换
- 例:Primitive2String.java
public class Primitive2String {
public static void main(String[] args) {
String intStr = "123";
// 把一个特定字符串转换成int变量
int it1 = Integer.parseInt(intStr);
int it2 = new Integer(intStr);
System.out.println(it2);
String floatStr = "4.56";
// 把一个特定字符串转换成float变量
float ft1 = Float.parseFloat(floatStr);
float ft2 = new Float(floatStr);
System.out.println(ft2);
// 把一个float变量转换成String变量
String ftStr = String.valueOf(2.345f);
System.out.println(ftStr);
// 把一个double变量转换成String变量
String dbStr = String.valueOf(3.344);
System.out.println(dbStr);
// 把一个boolean变量转换成String变量
String boolStr = String.valueOf(true);
System.out.println(boolStr.toUpperCase());
}
}
- 基本类型转换成字符串还有更简单的方法:将基本类型变量和""进行连接运算,系统会自动把基本类型变量转换成字符串。
// intStr的值为"5"
String intStr = 5 + "";
5.2 Object类
- Object类是所有类的父类,即所有类都继承自Object类。
5.2.1 equals方法
- Java程序中测试两个变量是否相等有两种方式:一个是利用
==
运算符,另一种是利用.equals()
方法 - 对于两个基本类型变量,且都是数值类型,只要两个变量的值相等,返回True
- 例:EqualTest.java
public class EqualTest {
public static void main(String[] args) {
int it = 65;
float f1 = 65.0f;
System.out.println("65和65.0是否相等?" + (it == f1));
char ch = 'A';
System.out.println("65和'A'是否相等?" + (it == ch));
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println("str1和str2是否相等?" + (str1 == str2));
System.out.println("str1是否equals str2? " + (str1.equals(str2)));
// 由于java.lang.String与EqualTest类没有继承关系
// 所以下面语句导致编译错误
// System.out.println("hello" == new EqualTest());
}
}
运行结果:
65和65.0是否相等?true
65和'A'是否相等?true
str1和str2是否相等?false
str1是否equals str2? true
- "hello"字符串直接量和
new String("hello")
的区别- 字符串直接量由常量池来管理
- 当使用
new String("hello")
时,JVM先使用常量池来管理"hello"直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。
public class StringCompareTest {
public static void main(String[] args) {
// s1直接引用常量池中的"Crazy Java"
String s1 = "CrazyJava";
String s2 = "Crazy";
String s3 = "Java";
// s4后面的字符串值可以在编译时就确定下来,直接引用常量池中的"Crazy Java"
String s4 = "Crazy" + "Java";
String s5 = "Cra" + "zy" + "Java";
// s6后面的字符串值不能在编译时就确定下来,不能引用常量池中的字符串
String s6 = s2 + s3;
// s7引用堆内存中新创建的String对象
String s7 = new String("CrazyJava");
System.out.println(s1 == s4);
System.out.println(s1 == s5);
System.out.println(s1 == s6);
System.out.println(s1 == s7);
}
}
运行结果:
true
true
false
false
重写equals方法
- Object默认提供的equals()只是比较对象的地址,即Object类的equals()方法比较的结果与==运算符比较的结果完全相同。因此,在实际应用中常常需要重写equals()方法。
- 正确地重写equals()方法应满足下列条件。- It is reflexive: For any non-null reference x ,x.equals(x) should return
true
. - It is symmetric: For any references x and y,
x.equals(y)
should returntrue
if and only ify.equals(x)
returnstrue
. - It is transitive: For any references x, y and z, if
x.equals(y)
returnstrue
andy.equals(z)
returnstrue
, thenx.equals(z)
returnstrue
. - It is consistent: If the objects to which x and y refer haven’t changed, then repeated calls to
x.equals(y)
return the same value. - For any non-null reference x,
x.equals(null)
should return false.
- It is reflexive: For any non-null reference x ,x.equals(x) should return
5.2.2 toString方法
toString()
方法是Object类里的一个实例方法,所有的Java类都是Object类的子类,因此所有的Java对象都具有toString()方法。- 例:PrintObject.java
class Person
{
private String name;
public Person(String name)
{
this.name = name;
}
}
public class PrintObject {
public static void main(String[] args) {
Person p = new Person("孙悟空");
// 打印p所引用的Person对象
System.out.println(p);
}
}
运行结果
c06.Person@7852e922
- 程序中的
System.out.println(p);
和System.out.println(p.toString());
效果完全一样。 toString()
方法是一个“自我描述”的方法,该方法通常用于实现这样一个功能:当程序员直接打印该对象时,系统将会输出对象的“自我描述”信息,用以告诉外界该对象具有的状态信息。- Object类提供的toSpring()方法总是返回该对象实现类的”类名+@+hashcode"值。如果用户需要自定义类能实现“自我描述”的功能,就必须重写Object类的
toString()
方法。 - 例:toStringTest.java
class Apple
{
private String color;
private double weight;
public Apple() {}
public Apple(String color, double weight)
{
this.color = color;
this.weight = weight;
}
// 省略color、weight的setter和getter方法
// ...
// 重写toString()方法,用于实现Applew对象的“自我描述”
public String toString()
{
return "一个苹果,颜色是:" + color + ", 重量是:" + weight;
}
}
public class ToStringTest {
public static void main(String[] args) {
Apple a = new Apple("红色", 5.68);
System.out.println(a);
}
}
运行结果:
一个苹果,颜色是:红色, 重量是:5.68
5.3 final修饰符
- final关键字可用于修饰类、变量和方法,用于表示它修饰的类、变量和方法不可改变。
- final修饰变量时,表示该变量一旦获得了初始值就不可被改变。final既可以修饰成员变量,也可以修饰局部变量、形参。
5.3.1 final成员变量
- final修饰的成员变量必须由程序员显式地指定初始值。
- final修饰的类变量、实例变量能指定初始值的位置如下:
- 类变量:静态初始化块、或声明该类变量时
- 实例变量:非静态初始化块、声明该实例变量时、构造器中
- 例:final修饰成员变量,FinalVariableTest.java
public class FinalVariableTest {
// 定义成员变量时指定默认值,合法
final int a = 6;
// 下面变量将在构造器或初始化块中分配初始值
final String str;
final int c;
final static double d;
// 既没有指定默认值,又没有在初始化块、构造器中指定初始值
// 下面定义的ch实例变量是不合法的
// final char ch;
// 初始化块,可对没有指定默认值的实例变量指定初始值
{
// 在初始化块中为实例变量指定初始值,合法
str = "Hello";
// 定义a实例变量时已经指定了默认值
// 不能为a重新赋值,因此下面赋值语句不合法
// a = 9;
}
// 静态初始化块,可对没有指定默认值的类变量指定初始值
static
{
// 在静态初始化块中为类变量指定初始值,合法
d = 5.6;
}
// 构造器,可对既没有指定默认值,又没有在初始化块中指定初始值的实例变量指定初始值
public FinalVariableTest()
{
// 如果在初始化块中已经对str指定了初始值
// 那么在构造器中不能对final变量重新赋值,下面赋值语句非法
// str = "java"
c = 5;
}
public void changeFinal()
{
// 普通方法不能为final修饰的成员变量赋值
// d = 1.2;
// 不能在普通方法中为final成员变量指定初始值
// ch = 'a';
}
public static void main(String[] args) {
FinalVariableTest ft = new FinalVariableTest();
System.out.println(ft.a);
System.out.println(ft.c);
System.out.println(ft.d);
}
}
5.3.2 final局部变量
- 系统不会对局部变量进行初始化,必须由程序员显式初始化。因此使用final修饰局部变量是,既可以在定义时指定默认值,也可以不指定默认值,但final修饰的变量只能赋值一次。
public class FinalLocalVariable {
public void test(final int a)
{
// 不能对final修饰的形参赋值,下面语句不合法
// a = 5;
}
public static void main(String[] args) {
// 定义final局部变量时指定默认值,则str变量无法重新赋值
final String str = "hello";
// 下面赋值语句不合法
// str = "Java";
// 定义final局部变量时没有指定默认值,则d变量可被赋值一次
final double d;
// 第一次赋初值
d = 5.6;
// 对final变量重复赋值,下面语句不合法
// d = 3.4;
}
}
5.3.3 final修饰基本类型变量和引用类型变量的区别
- final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。
- 对于引用变量,final只能保证这个引用变量所引用的地址不会改变,即一直引用同一个对象,但这个对象可以发生改变。
- 例:FinalReferenceTest.java
import java.util.Arrays;
class Person
{
private int age;
public Person() {}
public Person(int age)
{
this.age = age;
}
// 省略age的getter和setter方法
}
public class FinalReferenceTest {
public static void main(String[] args) {
// final修饰数组变量,iArr是一个引用变量
final int[] iArr = {5,6,12,9};
System.out.println(Arrays.toString(iArr));
// 对数组元素进行排序,合法
Arrays.sort(iArr);
System.out.println(Arrays.toString(iArr));
// 对数组元素进行赋值,合法
iArr[2] = -8;
System.out.println(Arrays.toString(iArr));
// 下面语句对iArr重新赋值,不合法
// iArr = null;
// final修饰Person变量,p是一个引用变量
final Person p = new Person(45);
// 改变Person对象的age实例变量,合法
p.setAge(23);
System.out.println(p.getAge());
// 下面语句对p重新赋值,非法
// p = null;
}
}
5.3.4 final方法
- final修饰的方法不可被重写,如果由于某些原因,不希望子类重写父类的某个方法,则可以使用final修饰该方法。
- 例:FinalMethodTest.java
public class FinalMethodTest
{
public final void test(){}
}
class Sub extends FinalMethodTest
{
// 下面定义将出现编译错误,不能重写final方法
public void test(){}
}
- 对于一个private方法,仅在当前类中可见,其子类无法访问该方法,所以子类无法重写该方法,如果子类中定义一个与父类private方法有相同方法名、相同形参列表、相同返回值类型的方法,也不是方法重写,只是定义了一个新方法。
public class PrivateFinalMethodTest
{
private final void test();
}
class Sub extends PrivateFinalMethodTest
{
// 下面的方法定义不会出现问题
public void test(){}
}
- final修饰的方法仅仅是不能被重写,并不是不能被重载,因此下面程序完全没有问题。
public class FinalOverload
{
public final void test(){}
public final void test(String arg){}
}
5.3.5 final类
- final修饰的类不可以有子类。为了保证某个类不可被继承,则可以使用final修饰这个类
5.3.6 不可变类
- 不可变类的意思是创建该类的实例后,该实例的实例变量是不可变的。Java提供的8个包装类和java.lang.String类都是不可变类。
- 如果需要创建自定义的不可变类,可遵守如下规则:
- 使用private和final修饰符来修饰该类的成员变量。
- 提供带参数构造器,用于根据传入参数来初始化类的成员变量。
- 仅为该类的成员变量提供getter方法,不提供setter方法(普通方法无法修改final修饰的成员变量)
- 如果有必要,重写Object类的hashCode()和equals()方法
- 例:Address.java
public class Address {
private final String detail;
private final String postCode;
// 在构造器里初始化两个实例变量
public Address()
{
this.detail = "";
this.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)
{
Address 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;
}
}
5.4 抽象类
5.4.1 抽象方法和抽象类
- 抽象方法是只有方法签名,没有方法实现的方法。
- 抽象类是一种模板模式,抽象类为所有子类提供了一个通用模板,子类可以在此模板基础上进行扩展。通过抽象类可以避免子类设计的随意性。
- 抽象方法和抽象类必须使用abstract修饰符来定义。
- 使用要点如下:
- 有抽象方法的类只能定义抽象类。
- 抽象类不能实例化,不能new一个抽象类。
- 抽象类可以包含属性、方法、构造方法(构造器、初始化块)、内部类(接口、枚举)。但是构造方法不能用来new实例,只能用来被子类调用。
- 抽象类只能用来继承。
- 抽象方法必须被子类实现。
- final和abstract不能同时使用。
- static和abstract不能同时修饰某个方法(但他们可以同时修饰内部类)
- private和abstract不能同时修饰某个方法
- 例:定义一个抽象类Person.
public abstract class Person {
public abstract String getDescription();
private String name;
public Person(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
- 定义Employee实例,Student实例继承Person类
import java.time.*;
public class Employee extends Person
{
private double salary;
private LocalDate hireDay;
public Employee(String name, double salary, int year, int month, int day) {
super(name);
this.salary = salary;
hireDay = LocalDate.of(year, month, day);
}
public double getSalary()
{
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public String getDescription()
{
return String.format("an employee with a salary of $%.2f", salary);
}
public void raiseSaraly(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
}
public class Student extends Person
{
private String major;
public Student(String name, String major) {
super(name);
this.major = major;
}
public String getDescription() {
return "a student majoring in " + major;
}
}
- 测试Person类:PersonTest.java
public class PersonTest {
public static void main(String[] args) {
Person[] people = new Person[2];
// fill the people array with Student and Employee objects
people[0] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
people[1] = new Student("Maria Morris", "computer science");
// print out names and descriptions of all Person objects
for (Person p:people)
System.out.println(p.getName() + ", " + p.getDescription());
}
}
5.5 接口
5.5.1 接口的概念
- In the Java language, an interface is not a class but a set of requirements for the classes that want to conform to the interface.
- 接口让规范和实现分离,让软件系统的各组件之间面向接口耦合,是一种松耦合的设计。
5.5.2 接口的定义
- 定义接口使用interface关键字。接口定义的基本语法如下:
[修饰符] interface 接口名 extends 父接口1,父接口2...
{
0个到多个常量定义...
0个到多个抽象方法定义...
0个到多个内部类、接口、枚举定义...
0个到多个默认方法或类方法定义...
}
- 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
- 由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法或默认方法)、内部类(包括内部接口、枚举)定义。
- 在接口中定义成员变量时,默认自动加上public static final修饰符。
- 例:定义一个Output接口
public interface Output {
// 接口里定义的成员变量只能是常量
int MAX_CACHE_LINE = 50;
// 接口里定义的普通方法只能是public的抽象方法
void out();
void getData(String msg);
// 在接口中定义默认方法,需要使用default修饰
default void print(String...msgs)
{
for (String msg : msgs)
{
System.out.println(msg);
}
}
// 在接口中定义默认方法,需要使用default修饰
default void test()
{
System.out.println("默认的test()方法");
}
// 在接口中定义类方法,需要使用static修饰
static String staticTest()
{
return "接口里的类方法";
}
}
5.5.3 接口的继承
- 接口支持多继承,一个接口继承多个父接口时,多个父接口之间以英文逗号隔开。
- 例:InterfactExtendsTest.java
interface interfaceA
{
int PROP_A = 5;
void testA();
}
interface interfaceB
{
int PROP_B = 6;
void testB();
}
interface interfaceC extends interfaceA, interfaceB
{
int PROP_C = 7;
void testC();
}
public class InterfaceExtendsTest
{
public static void main(String[] args)
{
System.out.println(interfaceC.PROP_A);
System.out.println(interfaceC.PROP_B);
System.out.println(interfaceC.PROP_C);
}
}
运行结果:
5
6
7
5.5.4 使用接口
- 接口不能用于创建实例,但接口可以用于声明引用类型变量。
- 接口的主要用途:
- 定义变量,也可用于进行强制类型转换。
- 调用接口中定义的常量
- 被其他类实现
- 类实现接口的语法格式如下:
[修饰符] class 类名 extends 父类 implements 接口1,接口2...
{
类体部分
}
- 一个类实现一个或多个接口之后,这个类必须完全实现这些接口所定义的全部抽象方法(就是要重写这些抽象方法)
- 例:定义一个Product接口和实现接口的Printer类
public interface Product {
int getProduceTime();
}
// 让Printer实现Output和Product接口
public class Printer implements Product, Output {
private String[] printData = new String[MAX_CACHE_LINE];
// 用以记录当前需打印的作业数
private int dataNum = 0;
@Override
public void out() {
// 只要还有作业,就继续打印
while(dataNum > 0)
{
System.out.println("打印机打印:" + printData[0]);
// 把作业队列整齐前移一位,并将剩下的作业数减1
System.arraycopy(printData, 1, printData, 0, --dataNum);
}
}
@Override
public void getData(String msg) {
if (dataNum >= MAX_CACHE_LINE)
{
System.out.println("输出队列已满,添加失败");
}
else
{
printData[dataNum++] = msg;
}
}
@Override
public int getProduceTime() {
return 45;
}
public static void main(String[] args) {
// 创建一个Printer对象,当成Output使用
Output o = new Printer();
o.getData("轻量级JavaEE");
o.getData("Craza Java");
o.out();
o.getData("Craza Android");
o.getData("Craza Ajax");
o.out();
// 调用Output接口中定义的默认方法
o.print("孙悟空","猪八戒","白骨精");
o.test();
Product p = new Printer();
System.out.println(p.getProduceTime());
// 所有接口类型的引用变量都可直接赋给Object类型的变量
Object obj = p;
}
}
运行结果:
打印机打印:轻量级JavaEE
打印机打印:Craza Java
打印机打印:Craza Android
打印机打印:Craza Ajax
孙悟空
猪八戒
白骨精
默认的test()方法
45
5.5.5 接口和抽象类
- 接口和抽象类的差别主要体现在二者设计目的上
- 接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式提供);对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务。当一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间应用接口时,接口是多个程序之间的通信标准。
- 抽象类体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,需要有进一步的完善。
5.5.6 面向接口编程
- 充分利用接口可以降低程序模块之间的耦合,从而提高系统的可扩展性和可维护性。
简单工厂模式
- 假设一个Computer类要组合一个输出设备,两种方案:一种组合一个Printer类,一种组合一个Output接口,再有接口组合一个Printer类或其他的输出设备。第二种方式就是”简单工厂“设计模式。(设计模式,就是对经常出现的软件设计问题的成熟解决方案)
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 OutputFactory {
public Output getOutput()
{
return new Printer();
// return new BetterPrinter();
}
public static void main(String[] args) {
OutputFactory of = new OutputFactory();
Computer c = new Computer(of.getOutput());
c.keyIn("轻量级avaEE");
c.keyIn("Crazy Java");
c.print();
}
}
- 当Printer对象切换到BetterPrinter对象时,只需要将
return new Printer();
改为return new BetterPrinter();
即可。
public class BetterPrinter implements Output {
private String[] printData = new String[MAX_CACHE_LINE * 2];
private int dataNum = 0;
@Override
public void out() {
while(dataNum > 0)
{
System.out.println("打印机打印:" + printData[0]);
System.arraycopy(printData, 1, printData, 0, --dataNum);
}
}
@Override
public void getData(String msg) {
if (dataNum >= MAX_CACHE_LINE * 2)
{
System.out.println("输出队列已满,添加失败");
}
else
{
printData[dataNum++] = msg;
}
}
}
命令模式
- 某个方法需要完成某一个行为,但这个行为的具体实现无法确定,必须等到执行该方法时才可以确定,即要把”处理行为“作为一个参数传入该方法?
- 考虑使用一个Command接口来定义一个方法,用这个方法来封装”处理行为“
public interface Command {
// 接口里定义的process方法用于封装“处理行为”,没有方法体
void process(int[] target);
}
- 下面是需要处理数组的处理类,在这个处理类中包含一个process()方法,这个方法无法确定处理数组的处理行为,所以定义该方法时使用了一个Command参数,这个参数负责对数组的处理行为。
public class ProcessArray {
public void process(int[] target, Command cmd)
{
cmd.process(target);
}
}
- 通过一个Command接口,实现了让ProcessArray类和具体”处理行为“的分离。
- 下面示范了对数组的两种处理方式:
public class CommandTest {
public static void main(String[] args)
{
ProcessArray pa = new ProcessArray();
int[] target = {3, -4, 6, 4};
// 第一次处理数组,具体处理行为取决于PrintCommand
pa.process(target, new PrintCommand());
System.out.println("--------------------");
// 第二次处理数组,具体处理行为取决于AddCommand
pa.process(target, new AddCommand());
}
}
public class PrintCommand implements Command {
@Override
public void process(int[] target) {
for (int tmp : target)
{
System.out.println("迭代输出目标数组的元素:" + tmp);
}
}
}
public class AddCommand implements Command {
@Override
public void process(int[] target) {
int sum = 0;
for(int tmp:target)
{
sum += tmp;
}
System.out.println("数组元素的总和是:" + sum);
}
}
运行结果:
迭代输出目标数组的元素:3
迭代输出目标数组的元素:-4
迭代输出目标数组的元素:6
迭代输出目标数组的元素:4
--------------------
数组元素的总和是:9
5.6 内部类
- 定义在其他类内部的类——内部类,包含内部类的类——外部类。
- 内部类的主要作用:
- 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
- 内部类成员可以直接访问外部类的私有数据,但外部类不能访问内部类的实现细节,如内部类的成员变量。
- 匿名内部类适合用于创建那些仅需要一次使用的类。
- 从语法角度看,内部类和外部类还有以下两点区别:
- 内部类比外部类可以多使用三个修饰符:private、protected、static
- 非静态内部类不能拥有静态成员
5.6.1 非静态内部类
- 成员内部类分为两种:静态内部类和非静态内部类。
- 例:在Cow类里定义一个Cowleg非静态内部类,并在CowLeg类的实例方法中直接访问Cow的private访问权限的实例变量。
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;
}
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
// 非静态内部类的实例方法
public void info()
{
System.out.println("当前牛腿颜色是:"
+ color + ",高:" + length);
// 直接访问外部类的private修饰的成员变量
System.out.println("本牛腿所在奶牛重:" + weight);
}
}
public void test()
{
CowLeg c1 = new CowLeg(1.12, "黑白相间");
c1.info();
}
public static void main(String[] args)
{
Cow cow = new Cow(378.9);
cow.test();
}
}
- 编译上面的程序,文件所在路径生成了两个class文件:
Cow.class
和Cow$CowLeg.class
。成员内部类(包括静态、非静态)的class文件总是这种形式:OuterClass$InnerClass.class
。 - 非静态内部类里可以直接访问外部类的private成员,因为在非静态内部类对象里,保存了一个它所处的外部类对象的引用。
- 在非静态内部类的方法内访问某个变量的查找顺序:该方法内->方法所在内部类中->内部类所在的外部类,都不存在,编译错误。
- 如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,可通过使用this、外部类类名.this作为限定来区分。
public class DiscernVariable {
private String prop = "外部类的实例变量";
private class InClass
{
private String prop = "内部类的实例变量";
public void info()
{
String prop = "局部变量";
// 通过外部类类名.this.varName 访问外部类实例变量
System.out.println("外部类的实例变量值:"
+ DiscernVariable.this.prop);
// 通过this.varName访问内部类实例的变量
System.out.println("内部类的实例变量值:"
+ this.prop);
// 直接访问局部变量
System.out.println("局部变量值:" + prop);
}
}
public void test()
{
InClass in = new InClass();
in.info();
}
public static void main(String[] args) {
new DiscernVariable().test();
}
}
运行结果:
外部类的实例变量值:外部类的实例变量
内部类的实例变量值:内部类的实例变量
局部变量值:局部变量
- 非静态内部类的成员变量不能被外部类直接使用,如果外部类需要访问非静态内部类的成员,必须显式创建非静态内部类对象来调用访问其实例成员。
public class Outer {
private int outProp = 9;
class Inner
{
private int inProp = 5;
public void accessOuterProp()
{
// 非静态内部类可以直接访问外部类的private成员变量
System.out.println("外部类的outProp值:"
+ outProp);
}
}
public void accessInnerProp()
{
// 外部类不能直接访问非静态内部类的实例变量
// 下面代码出现编译错误
// System.out.println("内部类的inProp值:" + inProp);
// 如需要访问内部类的实例变量,必须显式创建内部类对象
System.out.println("内部类的inProp值:"
+ new Inner().inProp);
}
public static void main(String[] args) {
// 执行下面代码,只创建了外部类对象,还未创建内部类对象
Outer out = new Outer();
out.accessInnerProp();
}
}
- 不允许外部类的静态成员直接使用非静态内部类。
- 不允许在非静态内部类里定义静态成员(静态方法、静态成员变量、静态初始化块)
5.6.2 静态内部类
- static修饰,属于外部类本身,而不属于外部类的某个对象——类内部类或静态内部类。
- 静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。
public class StaticInnerClassTest {
private int prop1 = 5;
private static int prop2 = 9;
static class StaticInnerClass
{
// 静态内部类里可以包含静态成员
private static int age;
public void accessOuterProp()
{
// 静态内部类无法访问外部类的实例变量,下面代码错误
// System.out.println(prop1);
// 下面代码正常
System.out.println(prop2);
}
}
}
- 外部类不能直接访问静态内部类的成员,可以使用静态内部类的类名来调用访问静态内部类的类成员,可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。
public class AccessStaticInnerClass
{
static class StaticInnerClass
{
private static int prop1 = 5;
private int prop2 = 9;
}
public void accessInnerProp()
{
//直接访问内部类的成员,下面代码错误
// System.out.println(prop1);
// 通过类名访问静态内部类的类成员
System.out.println(StaticInnerClass.prop1);
// 下面代码错误
// System.out.println(prop2);
// 通过实例访问静态内部类的实例成员
System.out.println(new StaticInnerClass().prop2);
}
}
- Java允许在接口里定义内部类,接口里定义的内部类默认使用public static修饰,接口里的内部类只能是静态内部类。
5.6.3 使用内部类
在外部类内部使用的内部类
- 注意不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员。
在外部类以外使用非静态内部类
- 在外部类以外的地方访问内部类,内部类不能使用private访问控制权限。
- 在外部类以外的地方定义内部类(静态、非静态)变量的语法格式如下:
OuterClass.InnerClass varName
- 在外部类以外的地方创建非静态内部类实例必须使用外部类实例和new来调用非静态内部类的构造器,语法如下:
OuterInstance.new InnerConstructor()
class Out
{
// 定义一个内部类,不使用访问控制符
// 即只有同一个包中的其他类可以访问该内部类
class In
{
public In(String msg)
{
System.out.println(msg);
}
}
}
public class CreateInnerInstance {
public static void main(String[] args) {
// 在外部类以外的地方创建非静态内部类的对象,并把它赋给非静态内部类类型的变量
Out.In in = new Out().new In("测试信息");
/*上面一行代码可改为如下三行代码:
使用OuterClass.InnerClass的形式定义内部类变量
Out.In in;
创建外部类实例,非静态内部类实例将寄生在该实例中
Out out = new Out();
通过外部类实例和new来调用内部类构造器创建非静态内部类实例
in = out.new In("测试信息");
*/
}
}
- 创建非静态内部类的子类时,要保证让子类构造器可以调用非静态内部类的构造器,而非静态内部类的构造器必须通过其外部类对象来调用。
public class SubClass extends Out.In
{
// 显式定义SubClass的构造器
public SubClass(Out out)
{
// 通过传入的Out对象显式调用In的构造器
out.super("hello");
}
}
- super代表调用In类的构造器,而out则代表外部类对象。
在外部类以外使用静态内部类
- 因为静态内部类是外部类类相关的,因此创建静态内部类对象时无须创建外部类对象。在外部类以外的地方创建静态内部类实例的语法如下:
new OuterClass.InnerConstructor()
class StaticOut
{
// 定义一个静态内部类,不使用访问控制符
static class StaticIn
{
public StaticIn()
{
System.out.println("静态内部类的构造器");
}
}
}
public class CreateStaticInnerInstance {
public static void main(String[] args) {
StaticOut.StaticIn in = new StaticOut.StaticIn();
}
}
- 创建静态内部类的子类
- 例如为静态内部类StaticIn类定义一个空的子类:
public class StaticSubClass extends StaticOut.StaticIn{}
5.6.4 局部内部类
- 在方法里定义的内部类——局部内部类。局部内部类不能放在外部类的方法以外的地方使用,所以不使用访问控制符和static修饰符。
- 如果需要用局部内部类定义变量、创建实例或派生子类,那么都只能在局部内部类所在的方法内进行。
public class LocalInnerClass {
public static void main(String[] args) {
// 定义局部内部类
class InnerBase
{
int a;
}
// 定义局部内部类的子类
class InnerSub extends InnerBase
{
int b;
}
// 创建局部内部类的对象
InnerSub is = new InnerSub();
is.a = 5;
is.b = 8;
System.out.println("InnerSub对象的a和b实例变量是:"
+ is.a + "," + is.b);
}
}
- 编译这个程序,生成了3个class文件:
LocalInnerClass.class
,LocalInnerClass$1InnerBase.class
,LocalInnerClass$1InnerSub.class
。 - 局部内部类的class文件的命名格式:
OuterClass$NInnerClass.class
。数字N是由于同一个类中可能存在同名的局部内部类(处于不同方法中),所以增加N来用于区分。
5.6.5 匿名内部类
- 匿名内部类适合创建那种只需要一次使用的类。
- 创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用。
- 定义匿名内部类的语法格式如下:
new InterfaceName()|SuperName(construction parameters)
{
inner class methods and data
}
- 匿名内部类必须继承一个父类或实现一个接口(最多一个)不能有显式的extends或implements子句。
- 关于匿名内部类还有如下两条规则:
- 匿名内部类不能是抽象类。
- 匿名内部类不能定义构造器,因为匿名内部类没有类名,无法定义构造器,但可以定义初始化块。
- 例:
public class AnonymousTest {
public void test(Product p)
{
System.out.println("购买了一个" + p.getName()
+ ", 花掉了" + p.getPrice());
}
public static void main(String[] args) {
AnonymousTest ta = new AnonymousTest();
// 调用test()方法时,需要一个Product对象作为参数
// 但是Product只是一个接口,无法直接创建对象,所以考虑将该实现类定义成一个独立类;
// 如果这个Product接口实现类只需一次调用,则可定义一个匿名内部类
ta.test(new Product()
{
public double getPrice()
{
return 567.8;
}
public String getName()
{
return "AGP显卡";
}
});
;
}
}
- 当通过实现接口来创建匿名内部类时,匿名内部类也不能显式创建构造器,因此匿名内部类只有一个隐式的无参构造器,故new接口名后的括号不能传入参数值。
- 当通过集成父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器(拥有相同的形参列表)。
abstract class Device
{
private String name;
public abstract double getPrice();
public Device() {}
public Device(String name)
{
this.name = name;
}
// 此处省略name的setter和getter方法
}
public class AnonymousInner {
public void test(Device d)
{
System.out.println("购买了一个" + d.getName()
+ ",花掉了" + d.getPrice());
}
public static void main(String[] args) {
AnonymousInner ai = new AnonymousInner();
// 调用有参数的构造器创建Device匿名实现类的对象
ai.test(new Device("电子示波器")
{
public double getPrice()
{
return 67.8;
}
});
// 调用无参数的构造器创建Device匿名实现类的对象
ai.test(new Device()
{
{
System.out.println("匿名内部类的初始化块");
}
// 实现抽象方法
public double getPrice()
{
return 56.2;
}
// 重写父类的实例方法
public String getName()
{
return "键盘";
}
});
}
}
5.7 Lambda表达式
5.7.1 Lambda表达式入门
interface Command {
// 接口里定义的process方法用于封装“处理行为”
void process(int[] target);
}
class ProcessArray {
public void process(int[] target, Command cmd)
{
cmd.process(target);
}
}
public class CommandTest {
public static void main(String[] args) {
ProcessArray pa = new ProcessArray();
int[] target = {3, -4, 6, 4};
// 处理数组,具体处理行为取决于匿名内部类
pa.process(target, new Command()
{
public void process(int[] target)
{
int sum = 0;
for(int tmp : target)
{
sum += tmp;
}
System.out.println("数组元素的总和是:" + sum);
}
});
}
}
- 可以使用lambda表达式来简化创建匿名内部类,例如将上面的代码改为如下形式:
public class CommandTest2 {
public static void main(String[] args) {
ProcessArray pa = new ProcessArray();
int[] array = {3, -4, 6, 4};
// 处理数组,具体处理行为取决于匿名内部类
pa.process(array,(int[] target)->
{
int sum = 0;
for(int tmp : target)
{
sum += tmp;
}
System.out.println("数组元素的总和是:" + sum);
});
}
}
- Lambda由三部分组成:
- 形参列表。形参列表允许省略形参类型,如果形参列表只有一个参数,甚至连形参列表的圆括号也可以省略。
- 箭头(->)
- 代码块。expression或{statement;}
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;
public class LambdaTest {
public static void main(String[] args) {
String[] planets = new String[] {"Mercury","Venus","Earth","Mars",
"Jupiter", "Saturn","Uranus", "Nepture"};
System.out.println(Arrays.toString(planets));
System.out.println("Sorted in dictionary order:");
Arrays.sort(planets);
System.out.println(Arrays.toString(planets));
System.out.println("Sorted by length:");
Arrays.sort(planets,(first,second) -> first.length() - second.length());
System.out.println(Arrays.toString(planets));
Timer t = new Timer(1000, event ->
System.out.println("The time is " + new Date()));
t.start();
// keep program running until user selects "ok"
JOptionPane.showMessageDialog(null, "Quit program");
System.exit(0);
}
}
interface Eatable
{
void taste();
}
interface Flyable
{
void fly(String weather);
}
interface Addable
{
int add(int a, int b);
}
public class LambdaQs {
// 调用该方法需要Eatable对象
public void eat (Eatable e)
{
System.out.println(e);
e.taste();
}
// 调用该方法需要Flyable对象
public void drive(Flyable f)
{
System.out.println("我正在驾驶:" + f);
f.fly("【碧空如洗的晴日】");
}
// 调用该方法需要Addable对象
public void test(Addable add)
{
System.out.println("5与3的和为:" + add.add(5, 3));
}
public static void main(String[] args) {
LambdaQs lq = new LambdaQs();
// Lambda表达式的代码块只有一条语句,可以省略花括号
lq.eat(()-> System.out.println("苹果的味道不错!"));
// Lambda表达式的形参列表只有一个形参,可以省略圆括号
lq.drive(weather ->
{
System.out.println("今天天气是:" + weather);
System.out.println("直升机飞行平稳");
});
// Lambda表达式的代码块只要一条语句,可以省略花括号
// 代码块中只有一条语句,即使该表达式需要返回值,也可以省略return关键字
lq.test((a,b)->a + b);
}
}
5.7.2 Lambda表达式与函数式接口
- Lambda表达式的类型,也被称为“目标类型”,Lmabda表达式的目标类型必须是“函数式接口/功能接口(functional interface)"或者称作”单抽象方法接口”。函数式接口代表只包含一个抽象方法的接口(还可以包含多个默认方法、类方法等)
- 由于Lambda表达式的结果就是被当成对象,因此程序中完全可以使用Lambda表达式进行赋值
// Runnable接口中只包含一个无参数的方法
// Lambda表达式代表的匿名方法实现了Runnable接口中唯一的、无参数的方法
// 因此下面的Lambda表达式创建了一个Runnable对象
Runnable r = () -> {
for(int i = 0; i < 100; i++)
{
System.out.println();
}
}
- 为了保证Lambda表达式的目标类型是一个明确的函数式接口,可以有如下三种常见方式:
- 将Lambda表达式赋值给函数式接口类型的变量
- 将Lambda表达式作为函数式接口类型的参数传给某个方法。
- 使用函数接口对Lambda表达式强制类型转换。
- Lambda表达式的本质就是使用简洁的语法来创建函数式接口的实例——避免了匿名内部类的繁琐。
5.7.3 方法引用与构造器引用
- Lambda表达式支持的方法引用和构造器引用
种类 | 示例 | 说明 | 对应的Lambda表达式 |
---|---|---|---|
引用类方法 | Class::staticMethod | 函数式接口中被实现方法的全部 参数传给该类方法作为参数 | (a,b...)-> 类名.类方法(a,b,...) |
引用特定对象的实例方法 | object::instanceMethod | 函数式接口中被实现方法的全部 参数传给该方法作为参数 | (a,b...)-> 特定方法.实例方法(a,b,...) |
引用某类对象的实例方法 | Class::instanceMethod | 函数式接口中被实现方法的 第一个参数作为调用者,后面的参数 全部传给该方法作为参数 | (a,b,...)->a.实例方法(b,...) |
引用构造器 | Class::new | 函数式接口中被实现方法的 全部参数传给该构造器作为参数 | (a,b,...) -> new 类名(a,b,...) |
引用类方法
- 定义如下函数式接口
@FunctionalInterface
// 该函数式接口包含一个convert()抽象方法,该方法负责将String参数转换为Integer
interface Converter{
Integer convert(String from);
}
- 使用Lambda表达式来创建一个Converter对象
Converter converter1 = from -> Integer.valueOf(from);
- 调用converter1对象的convert()方法将字符串转换为整数
Integer val = converter1.convert("99");
System.out.println(val);
- 上面Lambda表达式的代码块只有一行调用类方法的代码,可以用方法引用进行替换
Converter converter1 = Integer::valueOf;
引用特定对象的实例方法
- 使用Lamdba表达式创建Converter对象
Converter converter2 = from -> "fkit.org".indexOf(from);
- 调用converter1对象的convert()方法将字符串转换为整数
Integer value = converter2.convert("it");
System.out.println(value);
- 上面Lambda表达式的代码块只有一行调用"fkit.org"的indexOf()实例方法的代码,因此可以使用如下方法进行替换
Converter converter2 = "fkit.org"::indexOf;
引用某类对象的实例方法
- 例,定义如下函数式接口
@FunctionalInterface
interface MyTest
{
String test(String a, int b, int c);
}
- 该函数式接口中包含一个test()抽象方法,该方法负责根据String、int、int三个参数生成一个String返回值。下面代码使用Lambda表达式来创建一个MyTest对象
MyTest mt = (a,b,c) -> a.substring(b,c);
- 调用mt对象的test()方法
String str = mt.test("Java I Love You",2,9);
System.out.println(str);
- 上面Lambda表达式的代码块只有一行a.substring(b,c);因此可以使用如下方法引用进行替换
MyTest mt = String::substring;
构造器引用
- 定义如下函数式接口
@FuncitonalInterface
interface YourTest
{
JFrame win(String title);
}
- 该接口包含一个win()抽象方法,该方法负责根据String参数生成一个JFrame返回值。下面使用Lambda表达式来创建一个YourTest对象
YourTest yt = (String a) -> new JFrame(a);
- 调用yt对象的win()方法
JFrame jf = yt.win("我的窗口");
System.out.println(jf);
上面Lambda表达式可以用如下构造器引用进行替换
YourTest yt = JFrame::new;
5.7.4 Lambda表达式与匿名内部类的联系和区别
@FunctionalInterface
interface Displayable
{
// 定义一个抽象方法和默认方法
void display();
default int add(int a, int b)
{
return a + b;
}
}
public class LambdaAndInner
{
private int age = 12;
private static String name = "JavaNewWorld";
public void test()
{
String book = "CoreJava";
// 使用lambda表达式创建一个Displayable的对象
Displayable dis = () -> {
// 访问"effectively final"的局部变量
System.out.println("book局部变量为:" + book);
// 访问外部类的实例变量和类变量
System.out.println("外部类的age实例变量为:" + age);
System.out.println("外部类的name类变量为:" + name);
};
dis.display();
// 调用dis对象从接口中继承的add()方法
System.out.println(dis.add(3, 5));
}
public static void main(String[] args)
{
LambdaAndInner lambda = new LambdaAndInner();
lambda.test();
}
}
运行结果:
book局部变量为:CoreJava
外部类的age实例变量为:12
外部类的name类变量为:JavaNewWorld
8
- 相同点:
- 都可以直接访问局部变量,以及外部类的成员变量(类变量和实例变量)
- 创建的对象都可以直接调用从接口中继承的默认方法。
- 区别:
- 匿名内部类可以为任意接口创建实例,不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但Lambda表达式只能为函数式接口创建实例
- 匿名内部类可以为抽象类甚至普通类创建实例,但Lambda只能为函数式接口创建实例。
- 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法,但Lambda表达式的代码块不允许调用接口中定义的默认方法。
5.8 枚举类
- 实例有限且固定的类——枚举类(Enumeration Classes)
- 例:
public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE};
5.8.1 枚举类入门
- enum关键字(地位等同class、interface),用于定义枚举类。
- 枚举类是一个特殊的类,它可以有自己的成员变量、方法,可以实现一个或者多个接口,也可以定义自己的构造器。一个Java源文件中最多只能定义一个public访问权限的枚举类,且该Java源文件必须和该枚举类的类名相同。
- 例:
public enum Size
{
SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");
// 以SMALL("S")为例,相当于public static final Size SMALL = new Size("S");
private String abbreviation;
private Size(String abbreviation){this.abbreviation = abbreviation;}
public String getAbbreviation(){return abbreviation;}
}
- 枚举类与普通类的区别:
- 枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是默认继承Object类,因此枚举类不能显式继承其他父类。其中java.lang.Enum类实现了java.lang.Serializable和java.lang.Comparable两个接口。
- 使用enum定义、非抽象的枚举类默认会使用final修饰,因此枚举类不能派生子类。
- 枚举类的构造器只能使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰;
- 枚举类的所有实例必须在枚举类的第一行显式列出。
- 例:EnumTest.java
import java.util.*;
public class EnumTest {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("Enter a size:(SMALL, MEDIUM, LARGE, EXTRA_LARGE) ");
String input = in.next().toUpperCase();
Size size = Enum.valueOf(Size.class, input);
System.out.println("size=" + size);
System.out.println("abbreviation=" + size.getAbbreviation());
if (size == Size.EXTRA_LARGE)
System.out.println("Good job-- you paid attention to the _.");
}
}
- 枚举类默认有一个
values()
方法,返回该枚举类的所有实例,例如:
Size[] values = Size.values();
- 可以返回元素数组Size.SMALL,Size.MEDIUM, Size.LARGE,和Size.EXTRA_LARGE
- 枚举类都继承java.lang.Enum类,java.lang.Enum类中提供了如下方法:int compareTo(E other)
:该方法用于与指定枚举对象比较顺序,同一个枚举实例只能与相同类型的枚举实例进行比较。如果该枚举对象位于指定枚举对象之后,则返回正整数;如果该枚举对象位于指定枚举对象之前,则返回负整数,否则返回零。int ordinal()
:返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置,第一个枚举值的索引值为零)String toString()
:返回枚举常量的名称。static Enum valueOf(Class enumClass, String name)
:返回指定枚举类中指定名称的枚举值。
5.8.2 枚举类的成员变量、方法和构造器
- 例:
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;}
}
GenderTest.java
public class GenderTest {
public static void main(String[] args) {
Gender g = Gender.valueOf("FEMALE");
g.setName("女");
System.out.println(g + "代表:" + g.getName() );
// 此时设置name值将会提示参数错误
g.setName("男");
System.out.println(g + "代表:" + g.getName());
}
}