第6章 Java笔记 面向对象(下)

论坛 期权论坛 脚本     
匿名技术用户   2020-12-27 18:41   402   0

一、Java8增强的包装类


自动装箱,就是把一个基本类型变量直接赋给对应的包装类变量,或者Object变量;自动拆箱是直接把包装类对象赋给一个对应的基本类型变量。

包装类还可实现基本类型变量和字符串之间的转换。把字符串类型的值转换为基本类型的值有两种方式:

(1)利用包装类提供的parseXxx(String s)静态方法(除了Character之外的所有的包装类都提供了该方法)。

(2)利用包装类提供的Xxx(String s)构造器。

String类提供了多个重载valueOf()方法,用于将基本类型变量转换成字符串。

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());
 }
}

还可以将基本类型变量和“”进行连接运算,系统会自动把基本类型变量转换成字符串。


二、处理对象

1、打印对象和toString方法

toString()方法是Object类里的一个实例方法,所以所有的Java对象都具有toString()方法。此外所有的Java对象都可以和字符串进行连接运算,当Java对象和字符串进行连接运算时,系统自动调用Java对象toString()方法的返回值和字符串进行连接运算。

Object类提供的toString()方法总是返回该对象实现类的“类名+@+hashCode”值,因此如果需要自定义实现“自我描述”的功能,就必须重写Object类的toString()方法。

2、==和equals方法

当使用==进行判断两个变量是否相等时,如果两个变量是基本类型变量,且都是数值类型(不一定要求数据类型严格相同),只要两个变量值相等,就返回true。如果是两个引用类型变量,只有它们指向同一个对象时,==才会返回true。==不可用于比较类型上没有父子关系的两个对象。

public class EqualTest
{
 public static void main(String[] args)
 {
  int it = 65;
  float fl = 65.0f;
  // 将输出true
  System.out.println("65和65.0f是否相等?" + (it == fl));
  char ch = 'A';
  // 将输出true
  System.out.println("65和'A'是否相等?" + (it == ch));
  String str1 = new String("hello");
  String str2 = new String("hello");
  // 将输出false
  System.out.println("str1和str2是否相等?"
   + (str1 == str2));
  // 将输出true
  System.out.println("str1是否equals str2?"
   + (str1.equals(str2)));
  // 由于java.lang.String与EqualTest类没有继承关系,
  // 所以下面语句导致编译错误
//  System.out.println("hello" == new EqualTest());
 }
}
“hello”直接量和new String("hello")的区别:

当Java直接使用“hello”直接量时(包括可以在编译时就计算出来的字符串值),JVM将会使用常量池来管理这些字符串;当使用new String("hello")时,JVM会先使用常量池来管理"hello"直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。换句话说,new String("hello")一共产生了两个字符串对象。
常量池字符串示范如下:

public class StringCompareTest
{
 public static void main(String[] args)
 {
  // s1直接引用常量池中的"疯狂Java"
  String s1 = "疯狂Java";
  String s2 = "疯狂";
  String s3 = "Java";
  // s4后面的字符串值可以在编译时就确定下来
  // s4直接引用常量池中的"疯狂Java"
  String s4 = "疯狂" + "Java";
  // s5后面的字符串值可以在编译时就确定下来
  // s5直接引用常量池中的"疯狂Java"
  String s5 = "疯" + "狂" + "Java";
  // s6后面的字符串值不能在编译时就确定下来,
  // 不能引用常量池中的字符串
  String s6 = s2 + s3;
  // 使用new调用构造器将会创建一个新的String对象,
  // s7引用堆内存中新创建的String对象
  String s7 = new String("疯狂Java");
  System.out.println(s1 == s4); // 输出true
  System.out.println(s1 == s5); // 输出true
  System.out.println(s1 == s6); // 输出false
  System.out.println(s1 == s7); // 输出false
 }
}

JVM常量池保证相同的字符串直接量只有一个,不会产生多个副本。上述中s1、s4、s5所引用的字符串可以在编译期就确定下来,因此它们都将引用常量池中的同一个字符串对象。s1和s6不相等应该是因为s6并不是在编译期确定的。

equals()方法时Object类提供的一个实例方法,但是使用这个方法判断两个对象相等的标准与使用==运算符没有什么区别。事实上,String已经重写了Object的equals()方法,其判断两个字符串相等的标准是:只要两个字符串所包含的字符串序列相同,通过equals()比较将返回true,否则返回false。

重写equals示例:

class Person
{
 private String name;
 private String idStr;
 public Person(){}
 public Person(String name , String idStr)
 {
  this.name = name;
  this.idStr = idStr;
 }
 // 此处省略name和idStr的setter和getter方法。
 
 // 重写equals()方法,提供自定义的相等标准
 public boolean equals(Object obj)
 {
  // 如果两个对象为同一个对象
  if (this == obj)
   return true;
  // 只有当obj是Person对象
  if (obj != null && obj.getClass() == Person.class)
  {
   Person personObj = (Person)obj;
   // 并且当前对象的idStr与obj对象的idStr相等才可判断两个对象相等
   if (this.getIdStr().equals(personObj.getIdStr()))
   {
    return true;
   }
  }
  return false;
 }
}
上面的程序重写Person类的equals()方法,指定标准为:另一个对象必须是Person类的实例,且两个Person对象的idStr相等。
三、类成员

1、理解类成员

static关键字修饰的成员就是类成员,该关键字不能修饰构造器。static修饰的类成员属于整个类,不属于单个实例。类成员(包括方法、初始化块、内部类和枚举类)不能访问实例成员(包括成员变量、方法、初始化块、内部类和枚举类)。因为类成员是属于类的,类成员的作用域比实例成员的作用域更大,完全可能出现类成员已经初始化完成,但实例成员还不曾初始化的情况,如果允许类成员访问实例成员将会引起大量错误。

2、单例类

如果一个类始终只能创建一个实例,则这个类被称为单例类。

为了不让其他类随意创建某个类的对象,应该把该类的构造器用private修饰,要遵循的原则是:一旦把该类的构造器隐藏起来,就需要提供一个public方法作为该类的访问点,用于创建该类的对象,切该方法必须用static修饰(应为调用该方法之前还不存在对象,因此调用该方法的不可能是对象,只能是类)。

此外该类还必须缓存已经创建的对象,否则该类无法知道是否曾创建过对象。所以该类需要使用一个成员变量来保存曾经创建的对象,又因为该成员变量需要被上面的静态方法访问,故该成员变量必须使用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关键字可用于修饰类、变量和方法,用于表示不可改变。

1、final成员变量

final修饰的成员变量必须由程序员显式地指定初始值,系统不会对final成员进行隐式初始化。指定初始值的地方为:

类变量:必须在静态初始化块中指定初始值或者声明该类变量时指定初始值,而且只能在两个地方的其中之一指定。

实例变量:必须在非静态初始化块、声明该实例变量或者构造器中指定初始值,而且只能在三个地方的其中之一指定。

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);
 }
}
2、final局部变量

public class FinalLocalVariableTest
{
 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;
 }
}
3、final修饰基本类型变量和引用类型变量的区别

当使用final修饰基本类型变量时,不能对基本类型变量重新赋值。当使用final修饰引用变量时,只能保证这个引用类型变量所引用的地址不会改变,但这个对象完全可以发生改变。

class Person
{
 private int age;
 public Person(){}
 // 有参数的构造器
 public Person(int age)
 {
  this.age = age;
 }
 // 省略age的setter和getter方法
 // age的setter和getter方法
 public void setAge(int age)
 {
  this.age = age;
 }
 public int getAge()
 {
  return this.age;
 }
}
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;
 }
}
4、可执行“宏替换”的final变量
对于final变量,只要满足下面三个条件,这个final变量就不再是一个变量,而是相当于一个直接量,本质上也就是一个宏变量。
(1)使用final修饰
(2)在定义该final变量是指定初始值
(3)该初始值可以在编译时就被确定下来
public class FinalLocalTest
{
 public static void main(String[] args)
 {
  // 定义一个普通局部变量
  final int a = 5;
  System.out.println(a);
 }
}

此外,如果被赋值的表达式只是基本的算术表达式或字符串连接运算,没有访问普通变量,调用方法,那么该final变量同样被当成“宏变量”处理。

5、final方法
final修饰的方法不可被重写。
对于一个private方法,因为只能在当前类中可见,其子类无法访问该方法,所以子类无法重写该方法。如果子类定义了一个与父类private方法有相同方法名、相同参数列表、相同返回值类型的方法,也不是方法重写而是重新定义了一个方法。
因此,即使使用final修饰一个private访问权限的方法,依然可以在其子类中定义与该方法具有相同方法名、相同形参列表、相同返回类型的方法。

public class PrivateFinalMethodTest
{
 private final void test(){}
}
class Sub extends PrivateFinalMethodTest
{
 // 下面方法定义将不会出现问题
 public void test(){}
}
6、final类
final修饰的类不可以有子类
7、不可变类
不可变类是指创建该类的实例后,该实例的实例变量是不可变的。Java提供的8个包装类和java.lang.String类都是不可变类。
如果自定义不可变类,可遵守下面规则:
(1)使用private和final修饰符来修饰该类的成员变量。
(2)提供带参数构造器,用于根据传入参数来初始化类里的成员变量。
(3)仅为该类的成员变量提供getter方法,不提供setter方法。
(4)如果有必要,重写Object类的hashCode()和equals()方法。
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;
 }
}
在创建不可变类时应该注意引用类型变量,因为引用变量即使用final修饰也有可能该变指向的内容。
五、抽象类

1、抽象方法和抽象类

抽象方法和抽象类必须使用abstract修饰,有抽象方法的类只能定义成抽象类,抽象类可以没有抽象方法。

定义规则:

(1)抽象类和抽象方法必须用abstract修饰,抽象方法不能有方法体。

(2)抽象类不能被实例话,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。抽象类的构造器主要用于被其子类调用。

(3)含有抽象方法的类只能被定义成抽象类。

抽象类只能被继承,抽象方法必须有子类实现(重写)。而final修饰的类不能被继承,final修饰的方法不能被重写。因此final和abstract永远不能同时使用。

注意:abstract不能修饰成员变量、局部变量和构造器,抽象类里面的构造器只能是普通构造器。

static和abstract不能同时修饰某个方法,但是可以同时修饰内部类。

private和abstract不能同时修饰方法,因为abstract修饰的方法要被子类重写。

六、接口

1、接口的定义

接口定义使用interface关键字,基本语法如下:

[修饰符] interface 接口名 extends 父接口1,父接口2...
{
 零到多个常量定义...
 零到多个抽象方法定义...
 零到多个内部类、接口、枚举定义...
 零到多个默认方法或类方法定义...
}

接口规范:

(1)上述修饰符可以是public或者省略。

(2)接口只能继承接口不能继承类。

(3)接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法或默认方法)、内部类(包括内部接口、枚举)定义。

(4)接口里的所有成员都是public访问权限。定义接口成员时,可以省略访问控制修饰符,如果只能修饰符的话只能指定public。

(5)接口里的成员变量总是使用public static final修饰符(不管是否使用,系统都会默认),并且这些变量在定义时要指定初始化。如下面两行代码的结果完全一样:

int MAX_SIZE=50;
public static final int Max_SIZE=50;
(6)定义接口里的普通方法时,不管是否使用public abstract修饰符,接口里的普通方法总是使用public abstract来修饰。接口里的普通方法不能有方法实现(方法体);但类方法、默认方法都必须要有方法实现。

(7)默认方法必须使用default修饰,该方法不能用static修饰,无论程序是否指定,默认方法总是使用public修饰。类方法必须用static修饰,不可用default修饰,无论程序是否指定,类方法总是使用public修饰。

注意:接口里定义的内部类、内部接口、内部枚举默认都采用public static两个修饰符,不管定义时是否指定这两个修饰符,系统都会自动使用public static 对它们进行修饰。

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 "接口里的类方法";
 }
}
2、使用接口
[修饰符] class 类名 extends 父类 implements 接口1,接口2...
{
 类体部分
}
接口不能用于创建实例,但接口可以用于声明引用类型变量。当使用接口来声明引用类型变量时,这个引用类型变量必须引用到其实现类的对象。

一个类可以继承一个父类,并同时实现多个接口,implements部分必须放在extends部分之后。
一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法;

interface Product
{
 int getProduceTime();
}
// 让Printer类实现Output和Product接口
public class Printer implements Output , Product
{
 private String[] printData
  = new String[MAX_CACHE_LINE];
 // 用以记录当前需打印的作业数
 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)
  {
   System.out.println("输出队列已满,添加失败");
  }
  else
  {
   // 把打印数据添加到队列里,已保存数据的数量加1。
   printData[dataNum++] = msg;
  }
 }
 public int getProduceTime()
 {
  return 45;
 }
 public static void main(String[] args)
 {
  // 创建一个Printer对象,当成Output使用
  Output o = new Printer();
  o.getData("轻量级Java EE企业应用实战");
  o.getData("疯狂Java讲义");
  o.out();
  o.getData("疯狂Android讲义");
  o.getData("疯狂Ajax讲义");
  o.out();
  // 调用Output接口中定义的默认方法
  o.print("孙悟空" , "猪八戒" , "白骨精");
  o.test();
  // 创建一个Printer对象,当成Product使用
  Product p = new Printer();
  System.out.println(p.getProduceTime());
  // 所有接口类型的引用变量都可直接赋给Object类型的变量
  Object obj = p;
 }
}
注意:实现接口方法时必须使用public访问控制修饰符,因为接口里的方法都是public的,而子类(相当于实现类)重写父类方法时访问权限只能更大或者相等。

接口和抽象类在用法上的区别:

(1)接口里只能包含抽象方法、静态方法和默认方法,不能为普通方法提供方法实现;抽象类则完全可以包含普通方法。

(2)接口里只能定义静态常量,不能定义普通成员变量;抽象类则既可以定义普通成员变量,也可以定义静态变量。

(3)接口里不包含构造器;抽象类包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。

(4)接口里不能包含初始化块;但抽象类则完全可以包含初始化块。

(5)一个类最多只有一个直接父类,包括抽象类;但是一个类可以直接实现多个接口。

七、内部类

把一个类放在另一个类的内部定义就是内部类。作用为:

(1)内部类提供了更好的封装,不允许同一个包中的其他类访问该类。

(2)内部类成员可以直接访问外部类的私有数据,但是外部类不能访问内部类的实现细节,例如内部类的成员变量。

(3)匿名内部类适合用于创建那些仅需要一次使用的类。

(4)内部类比外部类多使用三个修饰符:private、protected、static——外部类不可以使用这三个修饰符。

(5)非静态内部类不能拥有静态成员。

1、非静态内部类

成员内部类与成员变量、方法、构造器和初始化块相似的类成员;局部内部类和匿名内部类不是类成员。

成员内部类分为两种:静态内部类(static修饰)和非静态内部类(无static修饰)

注意:外部类的上一级程序单元是包,所以它有两个作用域:同一个包内和任何位置。因此使用public或者省略访问控制符。

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 info()
  {
   System.out.println("当前牛腿颜色是:"
    + color + ", 高:" + length);
   // 直接访问外部类的private修饰的成员变量
   System.out.println("本牛腿所在奶牛重:" + weight);   //①
  }
 }
 public void test()
 {
  CowLeg cl = new CowLeg(1.12 , "黑白相间");
  cl.info();
 }
 public static void main(String[] args)
 {
  Cow cow = new Cow(378.9);
  cow.test();
 }
}
编译上述文件会产生两个class文件。外部类:Cow.class 内部类:Cow$CowLeg.class。
非静态内部类可以直接访问外部类的private成员是因为在非静态内部类对象里保存了一个它所寄生的外部类对象的引用(当调用非静态内部类的实例方法时,必须有一个非静态内部类实例,非静态内部类实例必须寄生在外部类实例里)。

当在非静态内部类的方法内访问某个变量时,系统优先在该方法内查找是否存在该名字的局部变量,如果存在就使用该变量;如果不存在,就在该方法所在的内部类查找,找到就用,找不到就到该内部类所在的外部类中查找是否存在该名字的成员变量,如果存在就用,依然不存在的话就编译报错。

通过OutterClass.this.propName的形式访问外部类的实例变量,通过this.propName的形式非静态内部类实例变量。

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();
 }
}
如果外部类需要访问非静态内部类的成员,则必须显式创建非静态内部类对象来调用访问其实例成员。
注意:非静态内部类对象必须寄生在外部类对象里,而外部类对象则不必一定有非静态内部类对象寄生其中。

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);
 }
}

此外,也可以在接口里定义内部类,接口里定义的内部类默认使用public static 修饰。
3、使用内部类

3.1 在外部类以外使用非静态内部类

在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:

OuterClass.InnerClass varName;
在外部类之外的地方创建非静态内部类实例的语法如下(非静态内部类的构造器必须通过其外部类对象来调用):

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("测试信息");
  /*
  上面代码可改为如下三行代码:
  使用OutterClass.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");
 }
}
上述代码中out代表外部类的对象,super代表调用In类的构造器。

3.2 在外部类以外使用静态内部类

在外部类以外的地方创建静态内部类实例的语法如下:

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();
  /*
  上面代码可改为如下两行代码:
  使用OutterClass.InnerClass的形式定义内部类变量
  StaticOut.StaticIn in;
  通过new来调用内部类构造器创建静态内部类实例
  in = new StaticOut.StaticIn();
  */
 }
}
4、局部内部类

如果把一个内部类放在放在方法里面定义,则这个内部类就是一个局部内部类,局部内部类仅在这个方法里有效。

局部内部类不能使用访问控制符和static修饰符修饰。

注意:不论是局部变量还是局部内部类,它们的上一级都是方法而不是类,使用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);
 }
}

5、匿名内部类

匿名内部类适合创建那种只需要一次使用的类,创建一个匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用。定义格式如下:

new 实现接口()|父类构造器(实参列表)
{
 //匿名内部类的类体部分
}
可以看出,匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或实现一个接口。

应遵循的规则:

(1)匿名内部类不能是抽象类。

(2)匿名内部类不能定义构造器。但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。

实现接口的匿名内部类:

interface Product
{
 public double getPrice();
 public String getName();
}
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参数,
  // 此处传入其匿名实现类的实例
  ta.test(new Product()
  {
   public double getPrice()
   {
    return 567.8;
   }
   public String getName()
   {
    return "AGP显卡";
   }
  });
 }
}
继承的内部类:

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匿名实现类的对象
  Device d = new Device()//这里就是个匿名内部类,不一定非要定义在方法参数的括号中
  {
   // 初始化块
   {
    System.out.println("匿名内部类的初始化块...");
   }
   // 实现抽象方法
   public double getPrice()
   {
    return 56.2;
   }
   // 重写父类的实例方法
   public String getName()
   {
    return "键盘";
   }
  };
  ai.test(d);
 }
}
如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了final修饰。被匿名内部类访问的局部变量必须使用final修饰。

interface A
{
 void test();
}
public class ATest
{
 public static void main(String[] args)
 {
  int age = 8;     // ①
  // 下面代码将会导致编译错误
  // 由于age局部变量被匿名内部类访问了,因此age相当于被final修饰了
  age = 2;
  A a = new A()
  {
   public void test()
   {
    // 在Java 8以前下面语句将提示错误:age必须使用final修饰
    // 从Java 8开始,匿名内部类、局部内部类允许访问非final的局部变量
    System.out.println(age);
   }
  };
  a.test();
 }
}
八、Lambda表达式
1、Lambda表达式入门
Lambda表达式支持将代码块作为参数,允许使用更简洁的代码来创建只有一个抽象方法的接口的实例。

代码对比:

public class CommandTest
{
 public static void main(String[] args)
 {
  ProcessArray pa = new ProcessArray();
  int[] array = {3, -4, 6, 4};
  // 处理数组,具体处理行为取决于匿名内部类
  pa.process(array , new Command()              //从这里开始利用匿名内部类
   {            //进行方法代码的传入
    public void process(int[] target)
    {
     int sum = 0;
     for (int tmp : target )
     {
      sum += tmp;
     }
     System.out.println("数组元素的总和是:" + sum);
    }         
   });           //到这里结束
 }
}
public class CommandTest2
{
 public static void main(String[] args)
 {
  ProcessArray pa = new ProcessArray();
  int[] array = {3, -4, 6, 4};
  // 处理数组,具体处理行为取决于匿名内部类
  pa.process(array , (int[] target)->{    //Lambda表达式从这里开始
    int sum = 0;
    for (int tmp : target )
    {
     sum += tmp;
    }
    System.out.println("数组元素的总和是:" + sum);
   });            //到这里结束
 }
}
上述两段代码对比可以知道Lambda表达式只要给出重写的方法括号以及括号里的形参列表即可。当使用Lambda表达式代替匿名内部类创建对象时,Lambda表达式的代码块将会代替实现抽象方法的方法体,Lambda表达式相当于一个匿名方法。主要由三部分组成:

(1)形参列表。形参列表允许省略形参类型。如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略。

(2)箭头(->)。必须通过英文中画线号和大于号组成。

(3)代码块。如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号。Lambda表达式代码块只有一条return语句,甚至可以省略return关键字。Lambda表达式需要返回值,而它的代码块中仅有一条省略了return的语句,Lambda表达式会自动返回这条语句的值。
2、Lambda表达式与函数式接口
Lambda表达式的类型,也被称为“目标类型(target type)”,必须是“函数式接口(functional interface)”。函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法、但只能声明一个抽象方法。例如:Runnable、ActionListener等都是函数式接口。

通过Lambda表达式来创建函数式接口的实例时,该表达式创建出来的对象的目标类型就是这个函数式接口。
@FunctionalInterface
interface FkTest
{
 void run();
}

public class LambdaTest
{
 public static void main(String[] args)
 {
  // Runnable接口中只包含一个无参数的方法
  // Lambda表达式代表的匿名方法实现了Runnable接口中唯一的、无参数的方法
  // 因此下面的Lambda表达式创建了一个Runnable对象
  Runnable r = () -> {
   for(int i = 0 ; i < 100 ; i ++)
   {
    System.out.println();
   }
  };
//  // 下面代码报错: 不兼容的类型: Object不是函数接口
//  Object obj = () -> {
//   for(int i = 0 ; i < 100 ; i ++)
//   {
//    System.out.println();
//   }
//  };

  Object obj1 = (Runnable)() -> {
   for(int i = 0 ; i < 100 ; i ++)
   {
    System.out.println();
   }
  };

  // 同样的Lambda表达式可以被当成不同的目标类型,唯一的要求是:
  // Lambda表达式的形参列表与函数式接口中唯一的抽象方法的形参列表相同
  Object obj2 = (FkTest)() -> {
   for(int i = 0 ; i < 100 ; i ++)
   {
    System.out.println();
   }
  };

 }
}
注意:@FunctionalInterface注解通常放在接口定义前面,该注解用于高速编译器——检查该接口必须是函数式接口,否则编译器报错。

Lambda表达式的限制:

(1)Lambda表达式的目标类型必须是明确的函数式接口。

(2)Lambda表达式只能为函数式接口创建对象。Lambda表达式只能实现一个方法,因此它只能为只有一个抽象方法的接口(函数式接口)创建对象。

为了保证Lambda表达式的目标类型是一个明确的函数式接口,可以有如下三种常见方式。

(1)将Lambda表达式赋值给函数式接口类型的变量。

(2)将Lambda表达式作为函数式接口类型的参数传给某个方法。

(3)使用函数式接口对Lambda表达式进行强制类型转换。

同样的Lambda表达式的目标类型完全可能发生变化——唯一的要求是,Lambda表达式实现的匿名方法与目标类型(函数式接口)中唯一的抽象方法有相同的形参列表。

3、方法引用与构造器引用

方法引用和构造器引用都需要两个英文冒号

import javax.swing.*;
@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 = (String a) -> new JFrame(a);
  // 构造器引用代替Lambda表达式。
  // 函数式接口中被实现方法的全部参数传给该构造器作为参数。
  YourTest yt = JFrame::new;
  JFrame jf = yt.win("我的窗口");
  System.out.println(jf);
 }
}
九、枚举类

1、枚举类入门

enum关键字与class、interface的地位相同,用于定义枚举类。一个Java源文件最多只能定义一个public访问权限的枚举类,且该Java源文件也必须和该枚举类的类名相同。

枚举类与普通类的区别:

(1)枚举类默认继承java.lang.Enum类,而不是Object类,因此枚举类不能显式继承其他父类。

(2)使用enum定义的枚举类默认使用final修饰,因此枚举类不能派生子类。

(3)枚举类的构造器只能用private修饰,如果省略了访问控制符则默认是private修饰。

(4)枚举类的所有实例必须在第一行显式列出,否则这个枚举类用于不能产生实例。这些实例系统会自动添加public static final修饰。

枚举类默认提供了一个values()方法,该方法可以很方便遍历所有的枚举值。

如果需要使用该枚举类的某个实例,则可以使用EnumClass.variable的形式。

public enum SeasonEnum
{
 // 在第一行列出4个枚举实例
 SPRING,SUMMER,FALL,WINTER;
}
public class EnumTest
{
 public void judge(SeasonEnum s)
 {
  // switch语句里的表达式可以是枚举值
  switch (s)
  {
   case SPRING:
    System.out.println("春暖花开,正好踏青");
    break;
   case SUMMER:
    System.out.println("夏日炎炎,适合游泳");
    break;
   case FALL:
    System.out.println("秋高气爽,进补及时");
    break;
   case WINTER:
    System.out.println("冬日雪飘,围炉赏雪");
    break;
  }
 }
 public static void main(String[] args)
 {
  // 枚举类默认有一个values方法,返回该枚举类的所有实例
  for (SeasonEnum s : SeasonEnum.values())
  {
   System.out.println(s);
  }
  // 使用枚举实例时,可通过EnumClass.variable形式来访问
  new EnumTest().judge(SeasonEnum.SPRING);
 }
}
2、枚举类的成员变量、方法和构造器

枚举类的实例只能是枚举值,而不是随意通过new来创建枚举类对象。

public class GenderTest
{
 public static void main(String[] args)
 {
  // 通过Enum的valueOf()方法来获取指定枚举类的枚举值
  Gender g = Enum.valueOf(Gender.class , "FEMALE");
  // 直接为枚举值的name实例变量赋值
  g.name = "女";
  // 直接访问枚举值的name实例变量
  System.out.println(g + "代表:" + g.name);
 }
}
一旦为枚举类显式定义了带参数的构造器,列出枚举值时就必须对应地传入参数。

public enum Gender
{
 // 此处的枚举值必须调用对应构造器来创建
 MALE("男"),FEMALE("女");
 private final String name;
 // 枚举类的构造器只能使用private修饰
 private Gender(String name)
 {
  this.name = name;
 }
 public String getName()
 {
  return this.name;
 }
}
在枚举类中列出枚举值时,实际上就是调用构造器创建枚举类对象,只是这里无须使用new关键字,也无须显式调用构造器。

3、实现接口的枚举类

枚举类实现接口和普通类一样,如果需要每个枚举值在调用接口抽象方法时呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法。

public interface GenderDesc
{
 void info();
}
public enum Gender implements GenderDesc
{
 // 此处的枚举值必须调用对应构造器来创建
 MALE("男")
 // 花括号部分实际上是一个类体部分
 {
  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(
   "这是一个用于用于定义性别的枚举类");
 }
}
4、包含抽象方法的枚举类

枚举类定义抽象方法时不能使用abstract关键字将枚举类定义成抽象类(系统会自动添加abstract关键字)。只有非抽象的枚举类才默认使用final修饰。

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;
  }
 };
 // 为枚举类定义一个抽象方法
 // 这个抽象方法由不同的枚举值提供不同的实现
 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));
 }
}
十、对象与垃圾回收
1、对象在内存中的状态

可达状态:当一个对象被创建后,若有一个或一个以上的引用变量引用它,则这个对象在程序中就是可达状态。

可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它就进入可恢复状态。此时系统回收机制准备回收该对象所占用的内存,在回收该对象之前系统会调用可恢复状态对象的finalize()方法进行资源清理。如果系统在调用finalize()方法时重新让一个引用变量引用该对象,则这个对象会再次变为可达状态;否则该对象将进入不可达状态。

不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize()方法后依然没有使该对象变为可达状态,那么这个对象将永久性的失去引用,最后变为不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。

强制系统垃圾回收有如下两种方式:

(1)调用System类的gc()静态方法:System.gc()。

(2)调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()。

强制系统垃圾回收只是通知系统进行垃圾回收,但是系统是否进行垃圾回收依然不确定。

2、finalize方法

finalize()方法的4个特点:

(1)永远不要主动调用某个对象的finalize()方法,该方法应该交给垃圾回收机制调用。

(2)finalize()方法何时被调用,是否被调用具有不确定性,不是一定会执行的方法。

(3)当JVM执行可恢复对象的finalize()方法时,可能使该对象或系统中其他对象重新变成可达状态。

(4)当JVM执行finalize()方法时出现异常时,垃圾回收机制不会报告异常,程序继续执行。

注意:由于finalize()方法并不一定会被执行,因此如果想清理某个类里打开的资源,则不要放在finalize()方法找中进行清理。

十一、使用JAR文件

1、jar命令

1.1 创建jar文件:jar cf test.jar test

1.2 创建jar文件,并显示压缩过程:jar cvf test.jar test

1.3 不使用清单文件: jar cvfM test.jar test

1.4 自定义清单文件内容:jar cvfm test.jar manifest.mf test

1.5 查看jar包详细内容: jar tvf test.jar

1.6 查看jar包内容:jar tf test.jar

1.7 解压缩:jar xf test.jar

1.8 带提示信息解压缩:jar xvf test.jar

1.9 更新jar文件:jar uf test.jar Hello.class

1.10 更新时显示详细信息:jar uvf test.jar Hello.class










分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:7942463
帖子:1588486
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP