Java编程思想(第4版本)1-15章笔记

注:大三在读学生,趁着课余时间想多了解些Java知识,很多人推荐我读一读《Java编程思想》这本书,网上的口碑也非常不错,于是就买来看看。这本书读起来比较生涩,不适合初学Java的小白读,毕竟是元老级别的经典之作,密密麻麻的长篇概述看着头疼,我尽可能的精简出我认为比较核心的字句写到笔记上,所以建议有一定的Java语言基础后再读这本书或者看看我的这篇笔记博客,希望大家都能有所收获!

第1章 对象导论

这一章为全书的一个总体概述,详细内容,后续章节会逐渐详解!

1.1 抽象过程

​ 众所周知,所有的编程语言都提供抽象机制。人们所能够解决的问题的复杂性,直接取决于抽象的类型质量。这里的“类型”就是是指:“所抽象的内容是什么?”,比如,最初的汇编语言是对底层机器的轻微抽象,而汇编语言的复杂程度大家都是知道的,因此,后来出现了较为简单的所谓“命令式”语言 (BASIC、C等),这些"命令式"就是对汇编语言的抽象。这些语言在汇编语言的基础上有了大幅改进,但是它们所做的主要抽象仍然要求在解决问题时要基于计算机的结构,而不是基于所要解决问题的结构 (非面向对象)。

​ Java是面向对象(Object-Oriented Programming,简记OOP)的程序设计语言,OOP允许根据问题来描述问题,而不是根据执行解决方案的计算机来描述问题

​ 纯粹的面向对象程序设计语言的5个基本特性:

  1. 万物皆对象:可以将任何事物,任何问题都归为对象。

  2. 程序是对象的集合,对象之间通过消息来告知彼此该做什么:想请求一个对象,就必须向该对象发送一条消息。更具体的来说,可以把“消息”看作对某个对象的方法发出的调用请求。

  3. 每个对象都拥有自己的其他对象所构成的存储不理解

  4. 每个对象都拥有自己的类型:每个对象都是某个类(class)的一个实例(instance)

  5. 某一特定类型的所有对象都可以接收同样的消息:"消息"就等同于该对象所能调用的方法,文字游戏不多说看代码!

    class Man{
           
        public void hello(){
           
            Sysout.out.println("hello");
        }
    }
    @Test
    public void test(){
           
        Man man01 = new Man();//Man类型的对象1
        Man man02 = new Man();//Man类型的对象2
        
        man01.hello();//man01和man02都可以调用hello方法
        man02.hello();
    }
    

    对象具有状态、行为和标识。个人理解是:每个对象都可以有内部数据(对象的属性),方法(方法产生行为),每个对象都可以唯一与其他对象区分开来,即每个对象在内存中都有一个唯一的地址

1.2 每个对象都有一个接口

  • 类描述了具有相同特性 (数据元素) 和行为 (功能) 的对象集合,一个类实际上就是一个数据类型。

  • 一旦类被建立,就可以随心所欲地创建类的任意个对象。

  • 接口确定了对某一特定对象所能发出的请求。

​ 那么怎样才能获取有用的对象呢?必须有某种方式产生对对象的请求,使对象完成各种任务。每个对象都只能完成某些请求,这些请求由对象的接口 (interface) 定义,决定接口的便是类型。这里举一个电灯泡为实例来做一个简单的比喻:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TnP8HI0M-1604286999320)(https://github.com/CodeFarmer1999/img_cloud/blob/master/img/image-20200524165517456.png?raw=true)]

Light lt = new Light();		//实例化对象
lt.on						//向对象发送on()调用对象方法

​ 此过程通常被概括为:向某个对象 “发送消息”(产生请求),这个对象便知道此消息的目的,然后执行对应的程序代码。上例中:类型/类的名称是Light,特定的Light对象的名称是lt,可以向Light对象发出的请求是:on()打开它、off()关闭它,将它调亮或调暗。以这种方式创建了一个Ligth对象,定义这个对象的 “引用”(lt),然后调用new方法来创建该类型的新对象。

1.3 每个对象都提供服务

​ 当你正视图开发或理解一个程序设计时,最好的方法就是将对象想象为 “服务提供者”,程序本身向用户提供服务。你的目标就是去拆功能键能够提供理想服务来解决问题的一系列对象。

​ 将对象看作是服务的提供者有一个附带的好处就是:它有助于提高对象的内聚性。高内聚是软件设计的基本质量要求之一。

1.4 被隐藏的具体体现

​ Java用三个关键字在类的内部设定边界:public、private、protected。这些访问指定词决定了紧随其后被定义的东西可以被谁使用,或者说某些东西对谁隐藏。

  • public:表示公有权限,该数据成员、成员函数是对所有用户开放的,所有用户都可以直接进行调用。
  • private:private表示私有权限,私有的意思就是除了该class自己之外,任何人都不可以直接使用,私有财产神圣不可侵犯嘛,即便是继承该类的子类,都不可以使用。
  • protected:表示保护权限,与private关键字作用相当,区别仅在于继承于该类的子类可以访问proteceted成员,对于该类的子类来说,该类的内部成员如同加上public权限修饰符一样。

​ 除了上述三种权限修饰符外,Java还有一种默认的访问权限default,当没有使用任何访问权限修饰关键字时,默认使用default权限。这种权限通常被称为包访问权限,因为在这种权限下,类可以访问在同一个包中的其他类的成员,但是在包之外,这些成员就如同被指定了private权限一样。

4种权限修饰符对比

同类 同包 不同包子类 不同包非子类
public 1 1 1 1
protected 1 1 1 0
default 1 1 0 0
private 1 0 0 0

1.5 继承

1.5.1 继承的实现

​ 问题:在创建一个类之后,如果有另外一个类与其具有很多相似的功能或属性,而我们需要重新创建一个新类是否有些繁琐?

​ **继承 (extends)**可以完美地解决上述问题:继承就是在父类已有的功能或属性的基础上,子类对父类进行扩充 (正所谓长江后浪推前浪,一浪更比一浪强)。子类只需要额外定义父类没有的功能或属性,而将那些与父类相同的功能或属性从父类继承过来即可。

上代码:

class Person {
     
    private String name;
    private int age;
    public String getName()
    {
     
        return name;
    }
    public void setName(String name)
    {
     
        this.name=name;
    }
    public int getAge()
    {
     
        return age;
    }
    public void setAge(int age)
    {
     
        this.age=age;
    }
}
class Student extends Person{
     
    private String schoolName;
    public String getSchoolName()
    {
     
        return schoolName;
    }
    public void setSchoolName(String schoolName)
    {
     
        this.schoolName=schoolName;
    }
}
public class Test2{
     
    public static void main(String[] args)
    {
     
        Student student=new Student();
        student.setName("海贼王");
        student.setAge(21);
        student.setSchoolName("河南科技大学");
        System.out.println("学校:"+student.getSchoolName() +" 姓名:"+student.getName()+"  年龄:"+student.getAge());
    }
}

1.5.2 继承的限制

1. 子类对象在进行实例化前首先调用父类构造方法,再调用子类构造方法实例化子类对象

class Person {
     
    private String name;
    private int age;
    public String getName()
    {
     
        return name;
    }
    public void setName(String name)
    {
     
        this.name=name;
    }
    public int getAge()
    {
     
        return age;
    }
    public void setAge(int age)
    {
     
        this.age=age;
    }
    //父类构造方法
    public Person()
    {
     
        System.out.println("父类的构造方法");
    }
}
class Student extends Person{
     
    private String schoolName;
    public String getSchoolName()
    {
     
        return schoolName;
    }
    public void setSchoolName(String schoolName)
    {
     
        this.schoolName=schoolName;
    }
    //子类构造方法
    public Student()
    {
     
        System.out.println("子类的构造方法");
    }
}
public class Test2{
     
    public static void main(String[] args)
    {
     
        Student student=new Student();
        student.setName("海贼王");
        student.setAge(21);
        student.setSchoolName("河南科技大学");
        System.out.println("学校:"+student.getSchoolName() +" 姓名:"+student.getName()+"  年龄:"+student.getAge());
    }
}

输出结果:

>>>	父类的构造方法
>>>	子类的构造方法
>>>	学校:河南科技大学	姓名:海贼王	年龄:21

实际在子类构造方法中,相当于隐含了一个语句super(),调用父类的无参构造。同时如果父类里没有提供无参构造,那么这个时候就必须使用super(参数)明确指明要调用的父类构造方法

2. Java只允许单继承不允许多继承(一个子类继承一个父类)

eg:错误的继承:

class A{
     
}
class B{
     
}
class C extends A,B{
     
}

C继承A和B的主要目的是拥有A和B中的操作,为了实现这样的目的,可以采用多层继承的形式完成。

class A{
     
}
class B extends A{
     
}
class C extends B{
     
}

Java中不允许多重继承,但是允许多层继承!多层继承一般不会超过三层

3. 在继承时,子类会继承父类的所有结构

在进行继承的时候,子类会继承父类的所有结构 (包括私有属性、构造方法、普通方法):

  • 显示继承:所有非私有操作属于显示继承(可以直接调用)。
  • 隐式继承:所有私有操作属于隐式继承 (不可以直接调用,需要通过其它形式调用 (get或者set) )。
class Person {
     
    private String name;
    private int age;
    public String getName()
    {
     
        return name;
    }
    public void setName(String name)
    {
     
        this.name=name;
    }
    public int getAge()
    {
     
        return age;
    }
    public void setAge(int age)
    {
     
        this.age=age;
    }
    public Person()
    {
     
        System.out.println("父类的构造方法");
    }
    public void fun()
    {
     
        System.out.println("好好学习。。。");
    }
}
class Student extends Person{
     
    private String schoolName;
    public String getSchoolName()
    {
     
        return schoolName;
    }
    public void setSchoolName(String schoolName)
    {
     
        this.schoolName=schoolName;
    }
    public Student()
    {
     
        System.out.println("子类的构造方法");
    }
}
public class Test2{
     
    public static void main(String[] args)
    {
     
        Student student=new Student();
        student.setName("海贼王");
        student.setAge(18);
        //隐式继承
        System.out.println("姓名:"+student.getName()+" 年龄:"+student.getAge());
        //显示继承
        student.fun();
    }
}

输出结果:

>>>	父类的构造方法
>>>	子类的构造方法
>>>	姓名:海贼王	年龄:21
>>> 好好学习。。。

​ 此时父类中的属性以及方法均被子类所继承,但是发现子类能够使用的是所有非private操作,而所有的private操作无法被直接使用,所以称为隐式继承。

1.5.3 实例扩展

典型例子

class A{
     
    public A()
    {
     
        System.out.println("1.父类A的构造方法");
    }
    {
     
        System.out.println("2.父类A的构造代码块");
    }
    static{
     
        System.out.println("3.父类A的静态代码块");
    }
}
public class B extends A{
     
    public B()
    {
     
        System.out.println("4.子类B的构造方法");
    }
    {
     
        System.out.println("5.子类B的构造代码块");
    }
    static{
     
        System.out.println("6.子类B的静态代码块");
    }
    //测试
    public static void main(String[] args)
    {
     
        System.out.println("7.start......");
        new B();
        System.out.println("8.end.....");
    }
}

输出结果:

>>> 3.父类A的静态代码块
>>> 6.子类B的静态代码块
>>> 7.start......
>>> 2.父类A的构造代码块
>>> 1.父类A的构造方法
>>> 5.子类B的构造代码块
>>> 4.子类B的构造方法
>>> 8.end.....

​ 主类B中的静态块优先于主方法执行,所以6应该在7前面执行,且B类继承于A类,所以先执行A类的静态块3,所以进入主方法前的执行顺序为:3 6
​ 进入主方法后执行7,new B()之后应先执行A的构造方法然后执行B的构造方法,但由于A类和B类均有构造代码块块,构造代码块又优先于构造方法执行即 2 1(A的构造家族) 5 4(B的构造家族),有多少个对象,构造家族就执行几次,题目中有两个对象 所以执行顺序为:3 6 7 2 1 5 4 2 1 5 4 8

1.5.4 单根继承

​ 在Java中所有的类都继承与Object类,也就是说Object类是所有类的基类。

单根继承的有点

  • 可以在每个对象上执行某些基本操作
  • 所有对象都很容易在堆上创建
  • 参数的传递也得到了极大的简化
  • 使垃圾回收器的实现变得容易得多
  • 由于所有对象都保证具有Object类型信息,因此不会因无法确定对象的类型而陷入僵局,这对于系统级操作(如异常处理)显得尤为重要

1.6 多态

1.6.1 多态的基本概念

所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用变量调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性

1.6.2 多态的优点

  • 消除类型之间的耦合关系
  • 可替换性
  • 可扩充性
  • 接口性
  • 灵活性
  • 简化性

1.6.3 多态存在的三个必要条件

  • 继承
  • 重写
  • 父类引用指向子类对象

比如:

Parent p = new Child();

​ 当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。

1.6.4 实例

详细说明请看注释:

public class Test {
     
    public static void main(String[] args) {
     
      show(new Cat());  		  // 以 Cat 对象调用 show 方法
      show(new Dog());  		  // 以 Dog 对象调用 show 方法
                
      Animal animal = new Cat();  // 向上转型  
      animal.eat();               // 调用的是 Cat 的 eat
      Cat cat = (Cat)a;        	  // 向下转型  
      cat.work();        		  // 调用的是 Cat 的 work
  }  
            
    public static void show(Animal a)  {
     
      	a.eat();  
        // 类型判断
        if (a instanceof Cat)  {
       		// 猫做的事情 
            Cat c = (Cat)a;  
            c.work();  
        } else if (a instanceof Dog) {
      	// 狗做的事情 
            Dog c = (Dog)a;  
            c.work();  
        }  
    }  
}
 
abstract class Animal {
       
    abstract void eat();  
}  
  
class Cat extends Animal {
       
    public void eat() {
       				//重写父类方法
        System.out.println("猫吃鱼");  
    }  
    public void work() {
       
        System.out.println("猫抓老鼠");  
    }  
}  
  
class Dog extends Animal {
       
    public void eat() {
       				//重写父类方法
        System.out.println("狗吃骨头");  
    }  
    public void work() {
       
        System.out.println("狗看家");  
    }  
}

执行以上程序,输出结果为:

>>> 猫吃鱼
>>> 猫抓老鼠
>>> 狗吃骨头
>>> 狗看家
>>> 猫吃鱼
>>> 猫抓老鼠

instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为:****

boolean result = obj instanceof Class
/*
	其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。
  注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。
*/

1.6.5 多态的实现方式

  • 重写
  • 接口
  • 抽象类和抽象方法

1.7 封装

1.7.1 封装的基本概念

​ 在面向对象程式设计方法中,封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。要访问该类的代码和数据,必须通过严格的接口控制。封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。

1.7.2 封装的优点

  • 良好的封装能够减少耦合。
  • 类内部的结构可以自由修改。
  • 可以对成员变量进行更精确的控制。
  • 隐藏信息,实现细节。

1.7.3 实现Java封装的步骤

  1. 修改属性的可见性来限制对属性的访问(一般限制为private)

    例如:

    public class Person {
           
        private String name;
        private int age;
    }
    
  2. 对每个值属性提供对外的公共方法访问,也就是创建一对赋取值方法,用于对私有属性的访问

    例如:

    public class Person{
           
        private String name;
        private int age;public int getAge(){
           
          return age;
        }public String getName(){
           
          return name;
        }public void setAge(int age){
           
          this.age = age;
        }public void setName(String name){
           
          this.name = name;
        }
    }
    

    采用 this 关键字是为了解决实例变量局部变量之间发生的同名的冲突。

1.7.4 实例

让我们来看一个java封装类的例子:

EncapTest.java

public class EncapTest{
     
 
   private String name;
   private String idNum;
   private int age;
 
   public int getAge(){
     
      return age;
   }
 
   public String getName(){
     
      return name;
   }
 
   public String getIdNum(){
     
      return idNum;
   }
 
   public void setAge( int newAge){
     
      age = newAge;
   }
 
   public void setName(String newName){
     
      name = newName;
   }
 
   public void setIdNum( String newId){
     
      idNum = newId;
   }
}

​ 以上实例中public方法是外部类访问该类成员变量的入口。通常情况下,这些方法被称为getter和setter方法。因此,任何要访问类中私有成员变量的类都要通过这些getter和setter方法。通过如下的例子说明EncapTest类的变量怎样被访问:

RunEncap.java 文件代码

public class RunEncap{
     
   public static void main(String args[]){
     
      EncapTest encap = new EncapTest();
      encap.setName("James");
      encap.setAge(20);
      encap.setIdNum("12343ms");
 
      System.out.print("Name : " + encap.getName()+ 
                             " Age : "+ encap.getAge());
    }
}

以上代码编译运行结果如下:

>>> Name : James Age : 20

1.7.5 面向对象的三个基本特征

多态、继承、封装被称为面向对象的三个基本特征

1.8 抽象类

1.8.1 抽象类的基本概念

​ 在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。在Java中,抽象类表示的是一种继承关系,一个类只能继承一个抽象类,而一个类却可以实现多个接口

1.8.2 抽象类

在Java语言中使用abstract class来定义抽象类。如下实例:

Employee.java

public abstract class Employee
{
     
   private String name;
   private String address;
   private int number;
   public Employee(String name, String address, int number)
   {
     
      System.out.println("Constructing an Employee");
      this.name = name;
      this.address = address;
      this.number = number;
   }
   public double computePay()
   {
     
     System.out.println("Inside Employee computePay");
     return 0.0;
   }
   public void mailCheck()
   {
     
      System.out.println("Mailing a check to " + this.name
       + " " + this.address);
   }
   public String toString()
   {
     
      return name + " " + address + " " + number;
   }
   public String getName()
   {
     
      return name;
   }
   public String getAddress()
   {
     
      return address;
   }
   public void setAddress(String newAddress)
   {
     
      address = newAddress;
   }
   public int getNumber()
   {
     
     return number;
   }
}

注意到该 Employee 类没有什么不同,尽管该类是抽象类,但是它仍然有 3 个成员变量,7 个成员方法和 1 个构造方法。 现在如果你尝试如下的例子:

**AbstractDemo.java **

public class AbstractDemo
{
     
   public static void main(String [] args)
   {
     
      /* 以下是不允许的,会引发错误 */
      Employee e = new Employee("George W.", "Houston, TX", 43);	//抽象类不能实例化
 
      System.out.println("\n Call mailCheck using Employee reference--");
      e.mailCheck();
    }
}

当你尝试编译AbstractDemo类时,会产生如下错误:

Employee.java:46: Employee is abstract; cannot be instantiated
      Employee e = new Employee("George W.", "Houston, TX", 43);
                   ^
1 error

1.8.3 继承抽象类

我们能通过一般的方法继承Employee类:

Salary.java

public class Salary extends Employee
{
     
   private double salary; //Annual salary
   public Salary(String name, String address, int number, double
      salary)
   {
     
       super(name, address, number);
       setSalary(salary);
   }
   public void mailCheck()
   {
     
       System.out.println("Within mailCheck of Salary class ");
       System.out.println("Mailing check to " + getName()
       + " with salary " + salary);
   }
   public double getSalary()
   {
     
       return salary;
   }
   public void setSalary(double newSalary)
   {
     
       if(newSalary >= 0.0)
       {
     
          salary = newSalary;
       }
   }
   public double computePay()
   {
     
      System.out.println("Computing salary pay for " + getName());
      return salary/52;
   }
}

​ 尽管我们不能实例化一个 Employee 类的对象,但是如果我们实例化一个 Salary 类对象,该对象将从 Employee 类继承 7 个成员方法,且通过该方法可以设置或获取三个成员变量。

AbstractDemo.java

public class AbstractDemo
{
     
   public static void main(String [] args)
   {
     
      Salary s = new Salary("Mohd Mohtashim", "Ambehta, UP", 3, 3600.00);
      Employee e = new Salary("John Adams", "Boston, MA", 2, 2400.00);
 
      System.out.println("Call mailCheck using Salary reference --");
      s.mailCheck();
 
      System.out.println("\n Call mailCheck using Employee reference--");
      e.mailCheck();
    }
}

以上程序编译运行结果如下:

Constructing an Employee
Constructing an Employee
Call mailCheck using  Salary reference --
Within mailCheck of Salary class
Mailing check to Mohd Mohtashim with salary 3600.0

Call mailCheck using Employee reference--
Within mailCheck of Salary class
Mailing check to John Adams with salary 2400.

1.8.4 抽象方法

​ 如果你想设计这样一个类,该类包含一个特别的成员方法,该方法的具体实现由它的子类确定,那么你可以在父类中声明该方法为抽象方法。Abstract 关键字同样可以用来声明抽象方法,抽象方法只包含一个方法名,而没有方法体。抽象方法没有定义,方法名后面直接跟一个分号,而不是花括号。

public abstract class Employee
{
     
   private String name;
   private String address;
   private int number;
   
   public abstract double computePay();
   
   //其余代码
}

声明抽象方法会造成以下两个结果:

  • 如果一个类包含抽象方法,那么该类必须是抽象类
  • 任何子类必须重写父类的抽象方法,或者声明自身为抽象类。

​ 继承抽象方法的子类必须重写该方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该抽象方法,否则,从最初的父类到最终的子类都不能用来实例化对象。

如果Salary类继承了Employee类,那么它必须实现computePay()方法:

**Salary.java **

public class Salary extends Employee
{
     
   private double salary; // Annual salary
  
   public double computePay()
   {
     
      System.out.println("Computing salary pay for " + getName());
      return salary/52;
   }
   //其余代码
}

1.8.5 抽象类总结规定

  • 抽象类不能被实例化(初学者很容易犯的错),如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。
  • 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
  • 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。
  • 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。
  • 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。

1.9 接口

1.9.1 接口的基本概念

​ 接口(英文:Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。

接口与类相似点

  • 一个接口可以有多个方法。
  • 接口文件保存在 .java 结尾的文件中,文件名使用接口名。
  • 接口的字节码文件保存在 .class 结尾的文件中。
  • 接口相应的字节码文件必须在与包名称相匹配的目录结构中。

接口与类的区别

  • 接口不能用于实例化对象。
  • 接口没有构造方法。
  • 接口中所有的方法必须是抽象方法。
  • 接口不能包含成员变量,除了 static 和 final 变量。
  • 接口不是被类继承了,而是要被类实现。
  • 接口支持多继承。

接口特性

  • 接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
  • 接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。
  • 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。

抽象类和接口的区别

  • \1. 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
  • \2. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
  • \3. 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
  • \4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

1.9.2 接口的声明

NameOfInterface.java

public interface NameOfInterface
{
     
   //任何类型 final, static 字段
   //抽象方法
   public void eat();
   public void travel();
}

接口有以下特性:

  • 接口是隐式抽象的,当声明一个接口的时候,不必使用abstract关键字。
  • 接口中每一个方法也是隐式抽象的,声明时同样不需要abstract关键字。
  • 接口中的方法都是公有的。

1.9.3 接口的实现

​ 当类实现接口的时候,类要实现接口中所有的方法。否则,类必须声明为抽象的类。类使用implements关键字实现接口。在类声明中,Implements关键字放在class声明后面。

1.9.4 实例

MammalInt.java

public class MammalInt implements Animal{
     
 
   public void eat(){
     
      System.out.println("Mammal eats");
   }
 
   public void travel(){
     
      System.out.println("Mammal travels");
   } 
 
   public int noOfLegs(){
     
      return 0;
   }
 
   public static void main(String args[]){
     
      MammalInt m = new MammalInt();
      m.eat();
      m.travel();
   }
}

重写接口中声明的方法时,需要注意以下规则

  • 类在实现接口的方法时,不能抛出强制性异常,只能在接口中,或者继承接口的抽象类中抛出该强制性异常。
  • 类在重写方法时要保持一致的方法名,并且应该保持相同或者相兼容的返回值类型。
  • 如果实现接口的类是抽象类,那么就没必要实现该接口的方法。

在实现接口的时候,也要注意一些规则

  • 一个类可以同时实现多个接口。
  • 一个类只能继承一个类,但是能实现多个接口。
  • 一个接口能继承另一个接口,这和类之间的继承比较相似。

1.9.5 接口的继承

​ 一个接口能继承另一个接口,和类之间的继承方式比较相似。接口的继承使用extends关键字,子接口继承父接口的方法。

下面的Sports接口被Hockey和Football接口继承:

public interface Sports
{
     
   public void setHomeTeam(String name);
   public void setVisitingTeam(String name);
}
 
// 文件名: Football.java
public interface Football extends Sports
{
     
   public void homeTeamScored(int points);
   public void visitingTeamScored(int points);
   public void endOfQuarter(int quarter);
}
 
// 文件名: Hockey.java
public interface Hockey extends Sports
{
     
   public void homeGoalScored();
   public void visitingGoalScored();
   public void endOfPeriod(int period);
   public void overtimePeriod(int ot);
}

​ Hockey接口自己声明了四个方法,从Sports接口继承了两个方法,这样,实现Hockey接口的类需要实现六个方法。相似的,实现Football接口的类需要实现五个方法,其中两个来自于Sports接口。

1.10 对象的创建和生命周期

1.10.1 对象的创建

​ Java完全采用了动态内存分配方式创建对象,即要创建新的对象时,就要使用new关键字来构建此对象的动态实例。

1.10.2 对象的生命周期

​ 像C++这样的语言,必须通过代码的方式来确定何时销毁对象,这可能会出现因不正确处理而导致的内存泄漏问题。Java提供了被称为 “垃圾回收器” 的机制,它可以自动发现对象何时不再被使用,并将其销毁。

1.11 总结

​ 《Think in Java》这本书的第一章节对象导论,主要是整体概括这本书关于Java的学习内容,以及面向对象语言的起源等等。接下来让我们开始进一步体会Java面向对象程序设计语言的风采把。

第2章 一切都是对象

2.1 引用操纵对象

​ 每种编程语言都有自己的操纵内存中元素的方式。在有些语言中,开发者需要注意将要处理的数据是什么类型,是直接操纵元素,还是某种基于特殊语法间接表示 (例如C或C++中的指针)来操纵对象?

​ 而在Java中一切都得到了简化,一切都可以被视为对象,因此只需要采用单一固定的语法即可。然而,尽管一切都看作对象,但操纵的标识符实际上是对象的一个 “引用”。通过 “引用” 来操纵对象,当我们想要修改对象本身时,我们操控的是对象的 “引用”,再由 “引用” 来操控对象

​ 为了便于理解,这里以电视机(对象),和遥控器(对象的引用) 为例,进行分析:当一个人(用户)在房间内走动,人(用户)可以通过遥控器(对象的引用)来操控电视机(对象),而不需要每次都去直接操控电视机(对象)本身。此外,即使没有电视机(对象),遥控器(引用)亦可存在。也就是说,拥有一个引用,并不一定需要拥有一个对象与它相关联

​ 因此,如果想操纵一个词或句子,就可以创建也给String引用:

String str;

​ 但是,这里所创建的只是引用,并不是对象。如果此时向 str 发送一个 “消息”,就会返回一个运行时错误。这是因为 str 此时并没有与任何对象像关联。因此,一种安全的做法就是创建一个引用时,就对其进行初始化,如:

// 创建一个引用,并初始化,不关联对象
String str = "abc";

// 为了形成对比,我们这里给一个String类型的引用,并使它去关联一个对象
String str = new String("abc");

​ 这里由于是String 类型字符串,所以可以直接使用带引号的文本进行初始化,如果是其他类型的引用,采用其他方式初始化,如:

Boolean boo = false;
Object obj = null;
Integer num = 0;
...

2.2 由引用关联对象

​ 一旦创建了了以用,就希望它能与一个对象相关联,通常Java中使用 new 关键字实现这一操作。new 关键字就代表 “创建一个新对象”。所以前面的例子可以写成:

String str = new String("abc");

这样就关联了一个 String类型的对象。

2.2.1 对象存储到什么地方

​ 程序运行时,对象是如何安置呢?内存如何分配呢?

一般有5个不同的地方可以存储数据:

  • 寄存器:这是最快的存储区,因为它位于处理器内部。但是,由于寄存器的数量极其有限,所以寄存器会根据需求来进行分配,我们不能直接控制。
  • 堆栈:位于通用RAM(随机访问存储器)中,通过堆栈指针可以从处理器获得直接支持。堆栈指针向下移动,分配新内存;堆栈指针向上移动,释放内存。这种处理方式效率上仅次于寄存器。创建程序时,Java系统需要知道存储在堆栈中所有内容的确切生命周期,以便于上下移动堆栈指针。这一约束限制了程序的灵活性,所以Java只将某些数据存储到堆栈中——特别是对象引用而Java对象并不存储在堆栈中
  • :一种通用的内存池(也位于RAM区),用于存放所有的Java对象。堆不同于堆栈的好处是:编译器不需要知道存储的数据在堆里存活多长时间。因此,在堆中分配存储有很大的灵活性。当 new 一个对象时,会自动在堆内进行存储分配。用堆存储的缺点是:相对于堆栈,进行存储分配会花掉更多的时间。
  • 常量存储:常量值通常直接存放在程序代码内部,这样做是出于安全考虑的,因为它们永远不会改变。在嵌入式系统中,常量本身会和其他部分隔离开,这种情况下,可以选择将其存放在ROM(只读存储器)中。
  • 非RAM存储:暂不详细概述。

2.2.2 9种基本类型(8大基本类型)

​ 由于 new 创建对象时,将对象放在里,而这种方式对于创建特别小的,简单的变量,往往不是很高效。因此对于这些基本类型,Java不用 new来创建,而是创建一个并非是引用的 “自动” 变量。这个变量直接存储值,并置于堆栈中,这种方式更高效。

基本数据类型关系图

存储单位换算

  • 1 Byte = 8 Bits
  • 1 KB = 1024 Bytes
  • 1 MB = 1024 KB
  • 1 GB = 1024 MB

9种基本数据类型和取值范围

基本类型 大小(位/bit) 字节数(byte) 最小值 最大值 默认值 包装器类型
boolean - - false true false Boolean
char 16 bits 2 bytes Unicode 0 Unicode 2^16-1 Character
byte 8 bits 1 byte -2^7 2^7-1 0 Byte
short 16 bits 2 bytes -2~15 2^15-1 0 Short
int 32 bits 4 bytes -2^31 2^31-1 0 Integer
long 64 bits 8 bytes -2^63 2^63-1 0 Long
float 32 bits 4 bytes 0.0 Fload
double 64 bits 8 bytes 0.0 Double
void - - - - - Void

注意:对于boolean值,在Java规范中并没有给出其储存大小,在《Java虚拟机规范》给出了4个字节,和boolean数组1个字节的定义,具体还要看虚拟机实现是否按照规范来,所以1个字节、4个字节都是有可能的。除了void之外,其他8种基本数据类型被称为八大基本数据类型

图中从左向右的转换都是隐式转换,无需再代码中进行强制转换。从右向左均要进行强制类型转换,才能通过编译。强制转换会丢失精度,如:

//从左到右
byte i = 12;
System.out.println("byte:"+i);
short i2 = i;
System.out.println("short:"+i2);
char j = '²';
System.out.println("char:"+j);
int j3 = j;
System.out.println("int:"+j3);
long j4 = j;
/*
	输出结果:
	
    byte:12
    short:12
    char:²
    int:178
*/

//从右到左
double i = 178.33;
System.out.println("double:"+i);
float i1 = (float) i;
System.out.println("float:"+i1);
byte i5 = (byte) i;
System.out.println("byte:"+i5);
char i6 = (char) i;
System.out.println("char:"+i6);
/*
	输出结果:
	
    double:178.33
	float:178.33
    byte:-78
	char:²
*/

高精度数字

  • BigInteger:支持任意精度的整数,可以表示任何大小的整数值。
  • BigDecimal:支持任何精度的定点数,可以用它进行精确的货币计算。

BigInteger和BigDecimal虽然大体上属于"包装器类"的范畴,但是二者都没有对应的基本类型。但是能作用于int 或 float 的操作,也同样能作用于BigInteger和BigDecimal.

2.3 永远不需要销毁对象

2.3.1 作用域

​ 大多数过程型程序设计语言都有作用域(scope)的概念,作用域决定了在其内部定义的变量名的可见性和生命周期,在C、C++和Java中,作用域使用花括号的位置决定,例如:

{
     //作用域起始
    int x = 12;
}//作用域终点

在作用域中定义的变量只能用于作用域结束之前。

2.3.2 对象的作用域

​ Java对象不具备和基本类型一样的生命周期,当用 new 创建一个Java对象时,它可以存活于作用域之外。比如使用如下代码:

{
     
    String str = new String("abc");
}//作用域终点

​ String对象的引用 str 在作用域的终点就消失了,然而,str 指向的String对象仍然继续占据内存空间。在这一小段代码中,我们无法在这个作用域之后访问这个对象,因为对它唯一的引用已经超出了作用域的范围。在后续章节,会详细介绍怎样传递和复制对象的引用。

​ 如果Java让对象继续存在,那么靠什么防止这些对象填满内存空间,阻塞程序呢?为此Java提供了也给垃圾回收器,用来监视new 创建的所有对象,并辨别那些是不会再被引用的对象,随后释放这些对象的内存空间,以便提供给其他新的对象使用。

​ 也就是说,在Java中,不需要担心内存回收问题,只需要创建对象,当对象不需要时,它所占据的内存空间会自动被释放。

2.4 创建新的数据类型:类

本小节不在详细介绍,有如果有Java基础,对这个并不陌生,无非就是新建其他数据类型而创建一个类class,《Think in Java》这本书确实不太适合对Java语言零基础的去阅读,如果零基础可以去哔哩哔哩这些平台找些入门的视频教程即可。

2.5 static 关键字详解

​ 这一小节,原书中这一章节就是简单概述了以下static,后续会继续探讨,但是由于书本内容比较分散,就在这里找了一篇博客,给大家详细介绍下Java中的static。

2.5.1 static 的作用和使用方式

static关键字主要有两种作用

  • 为某特定数据类型或对象分配单一的存储空间,而与创建对象的个数无关。

  • 实现某个方法或属性与类而不是对象关联在一起

static主要有4中使用情况

  • 静态成员变量
  • 静态成员方法
  • 静态代码块
  • 静态内部类

2.5.2 修饰成员变量

​ 在我们平时的使用当中,static最常用的功能就是修饰类的属性和方法,让他们成为类的成员属性和方法,我们通常将用static修饰的成员称为类成员或者静态成员,这句话挺起来都点奇怪,其实这是相对于对象的属性和方法来说的。请看下面的例子:(未避免程序太过臃肿,暂时不管访问控制)

public class Person {
     
    String name;
    int age;
    
    public String toString() {
     
        return "Name:" + name + ", Age:" + age;
    }
    
    public static void main(String[] args) {
     
        Person p1 = new Person();
        p1.name = "zhangsan";
        p1.age = 10;
        Person p2 = new Person();
        p2.name = "lisi";
        p2.age = 12;
        System.out.println(p1);
        System.out.println(p2);
    }
    
    /* 输出结果:
     * Name:zhangsan, Age:10
     * Name:lisi, Age:12
     */
}

上面的代码我们很熟悉,根据Person构造出的每一个对象都是独立存在的,保存有自己独立的成员变量,相互不会影响,他们在内存中的示意如下:

Java编程思想(第4版本)1-15章笔记_第1张图片

从上图中可以看出,p1和p2两个变量引用的对象分别存储在内存中堆区域的不同地址中,所以他们之间相互不会干扰。但其实,在这当中,我们省略了一些重要信息,相信大家也都会想到,对象的成员属性都在这了,由每个对象自己保存,那么他们的方法呢?实际上,不论一个类创建了几个对象,他们的方法都是一样的:

Java编程思想(第4版本)1-15章笔记_第2张图片

从上面的图中我们可以看到,两个Person对象的方法实际上只是指向了同一个方法定义。这个方法定义是位于内存中的一块不变区域(由jvm划分),我们暂称它为静态存储区。这一块存储区不仅存放了方法的定义,实际上从更大的角度而言,它存放的是各种类的定义,当我们通过new来生成对象时,会根据这里定义的类的定义去创建对象。多个对象仅会对应同一个方法,这里有一个让我们充分信服的理由,那就是不管多少的对象,他们的方法总是相同的,尽管最后的输出会有所不同,但是方法总是会按照我们预想的结果去操作,即不同的对象去调用同一个方法,结果会不尽相同。

我们知道,static关键字可以修饰成员变量和方法,来让它们变成类的所属,而不是对象的所属,比如我们将Person的age属性用static进行修饰,结果会是什么样呢?请看下面的例子:

public class Person {
     
    String name;
    static int age;
 
public String toString() {
     
        return "Name:" + name + ", Age:" + age;
    }
    
    public static void main(String[] args) {
     
        Person p1 = new Person();
        p1.name = "zhangsan";
        p1.age = 10;
        Person p2 = new Person();
        p2.name = "lisi";
        p2.age = 12;
        System.out.println(p1);
        System.out.println(p2);
    }
    
    /* 输出结果:
     * Name:zhangsan, Age:12
     * Name:lisi, Age:12
     */
}

我们发现,结果发生了一点变化,在给p2的age属性赋值时,干扰了p1的age属性,这是为什么呢?我们还是来看他们在内存中的示意:

Java编程思想(第4版本)1-15章笔记_第3张图片

我们发现,给age属性加了static关键字之后,Person对象就不再拥有age属性了,age属性会统一交给Person类去管理,即多个Person对象只会对应一个age属性,一个对象如果对age属性做了改变,其他的对象都会受到影响。我们看到此时的age和toString()方法一样,都是交由类去管理。

虽然我们看到static可以让对象共享属性,但是实际中我们很少这么用,也不推荐这么使用。因为这样会让该属性变得难以控制,因为它在任何地方都有可能被改变。如果我们想共享属性,一般我们会采用其他的办法:

public class Person {
     
    private static int count = 0;
    int id;
    String name;
    int age;
    
    public Person() {
     
        id = ++count;
    }
    
    public String toString() {
     
        return "Id:" + id + ", Name:" + name + ", Age:" + age;
    }
    
    public static void main(String[] args) {
     
        Person p1 = new Person();
        p1.name = "zhangsan";
        p1.age = 10;
        Person p2 = new Person();
        p2.name = "lisi";
        p2.age = 12;
        System.out.println(p1);
        System.out.println(p2);
    }
    /* 输出结果:
     * Id:1, Name:zhangsan, Age:10
     * Id:2, Name:lisi, Age:12
     */
}

上面的代码起到了给Person的对象创建一个唯一id以及记录总数的作用,其中count由static修饰,是Person类的成员属性,每次创建一个Person对象,就会使该属性自加1然后赋给对象的id属性,这样,count属性记录了创建Person对象的总数,由于count使用了private修饰,所以从类外面无法随意改变。

2.5.3 修饰成员方法

​ static的另一个作用,就是修饰成员方法。相比于修饰成员属性,修饰成员方法对于数据的存储上面并没有多大的变化,因为我们从上面可以看出,方法本来就是存放在类的定义当中的。static修饰成员方法最大的作用,就是可以使用"类名.方法名"的方式操作方法,避免了先要new出对象的繁琐和资源消耗,我们可能会经常在帮助类中看到它的使用:

public class PrintHelper {
     

    public static void print(Object o){
     
    	System.out.println(o);
    }

	public static void main(String[] args) {
     
   		PrintHelper.print("Hello world");
    }
}

上面便是一个例子(现在还不太实用),但是我们可以看到它的作用,使得static修饰的方法成为类的方法,使用时通过“类名.方法名”的方式就可以方便的使用了,相当于定义了一个全局的函数(只要导入该类所在的包即可)。不过它也有使用的局限,一个static修饰的类中,不能使用非static修饰的成员变量和方法,这很好理解,因为static修饰的方法是属于类的,如果去直接使用对象的成员变量,它会不知所措(不知该使用哪一个对象的属性)。

2.5.4 静态代码块

​ 在说明static关键字的第三个用法时,我们有必要重新梳理一下一个对象的初始化过程。以下面的代码为例:

package com.dotgua.study;

class Book{
     
    public Book(String msg) {
     
        System.out.println(msg);
    }
}

public class Person {
     

    Book book1 = new Book("book1成员变量初始化");
    static Book book2 = new Book("static成员book2成员变量初始化");
    
    public Person(String msg) {
     
        System.out.println(msg);
    }
    
    Book book3 = new Book("book3成员变量初始化");
    static Book book4 = new Book("static成员book4成员变量初始化");
    
    public static void main(String[] args) {
     
        Person p1 = new Person("p1初始化");
    }
    
    /* 输出结果:
     * static成员book2成员变量初始化
     * static成员book4成员变量初始化
     * book1成员变量初始化
     * book3成员变量初始化
     * p1初始化
     */
}

上面的例子中,Person类中组合了四个Book成员变量,两个是普通成员,两个是static修饰的类成员。我们可以看到,当我们new一个Person对象时,static修饰的成员变量首先被初始化,随后是普通成员,最后调用Person类的构造方法完成初始化。也就是说,在创建对象时,static修饰的成员会首先被初始化,而且我们还可以看到,如果有多个static修饰的成员,那么会按照他们的先后位置进行初始化。

实际上,static修饰的成员的初始化可以更早的进行,请看下面的例子:

class Book{
     
    public Book(String msg) {
     
        System.out.println(msg);
    }
}

public class Person {
     

    Book book1 = new Book("book1成员变量初始化");
    static Book book2 = new Book("static成员book2成员变量初始化");
    
    public Person(String msg) {
     
        System.out.println(msg);
    }
    
    Book book3 = new Book("book3成员变量初始化");
    static Book book4 = new Book("static成员book4成员变量初始化");
    
    public static void funStatic() {
     
        System.out.println("static修饰的funStatic方法");
    }
    
    public static void main(String[] args) {
     
        Person.funStatic();
        System.out.println("****************");
        Person p1 = new Person("p1初始化");
    }
    
    /* 输出结果
     * static成员book2成员变量初始化
     * static成员book4成员变量初始化
     * static修饰的funStatic方法
     * ***************
     * book1成员变量初始化
     * book3成员变量初始化
     * p1初始化
     */
}

在上面的例子中我们可以发现两个有意思的地方,第一个是当我们没有创建对象,而是通过类去调用类方法时,尽管该方法没有使用到任何的类成员,类成员还是在方法调用之前就初始化了,这说明,当我们第一次去使用一个类时,就会触发该类的成员初始化。第二个是当我们使用了类方法,完成类的成员的初始化后,再new该类的对象时,static修饰的类成员没有再次初始化,这说明,static修饰的类成员,在程序运行过程中,只需要初始化一次即可,不会进行多次的初始化。

回顾了对象的初始化以后,我们再来看static的第三个作用就非常简单了,那就是当我们初始化static修饰的成员时,可以将他们统一放在一个以static开始,用花括号包裹起来的块状语句中:

class Book{
     
    public Book(String msg) {
     
    	System.out.println(msg);
    }
}

public class Person {
     
	Book book1 = new Book("book1成员变量初始化");
	static Book book2;
    
	static {
     
		book2 = new Book("static成员book2成员变量初始化");
		book4 = new Book("static成员book4成员变量初始化");
	}
    
    public Person(String msg) {
     
    	System.out.println(msg);
    }
    
	Book book3 = new Book("book3成员变量初始化");
	static Book book4;
    
    public static void funStatic() {
     
    	System.out.println("static修饰的funStatic方法");
    }
 
    public static void main(String[] args) {
     
        Person.funStatic();
        System.out.println("****************");
        Person p1 = new Person("p1初始化");
	}
}

/* 输出结果:
* static成员book2成员变量初始化
* static成员book4成员变量初始化
* static修饰的funStatic方法
* ***************
* book1成员变量初始化
* book3成员变量初始化
* p1初始化
*/

我们将上一个例子稍微做了一下修改,可以看到,结果没有二致。

2.5.5 静态导包

​ 相比于上面的三种用途,第四种用途可能了解的人就比较少了,但是实际上它很简单,而且在调用类方法时会更方便。以上面的“PrintHelper”的例子为例,做一下稍微的变化,即可使用静态导包带给我们的方便:

/* PrintHelper.java文件 */
package com.dotgua.study;
 
public class PrintHelper {
     
 
    public static void Testprint(Object o){
     
        System.out.println(o);
    }
}
/* App.java文件 */
import static com.dotgua.study.PrintHelper.*;
 
public class App 
{
     
    public static void main( String[] args )
    {
     
        Testprint("Hello World!");	//直接使用即可
    }
}

/* 输出结果:
 * Hello World!
 */

上面的代码来自于两个java文件,其中的PrintHelper很简单,包含了一个用于打印的static方法。而在App.java文件中,我们首先将PrintHelper类导入,这里在导入时,我们使用了static关键字,而且在引入类的最后还加上了“.*”,它的作用就是将PrintHelper类中的所有类方法直接导入。不同于非static导入,采用static导入包后,在不与当前类的方法名冲突的情况下,无需使用“类名.方法名”的方法去调用类方法了,直接可以采用"方法名"去调用类方法,就好像是该类自己的方法一样使用即可。

2.5.6 总结

static是java中非常重要的一个关键字,而且它的用法也很丰富,主要有四种用法:

  1. 用来修饰成员变量,将其变为类的成员,从而实现所有对象对于该成员的共享;
  2. 用来修饰成员方法,将其变为类方法,可以直接使用“类名.方法名”的方式调用,常用于工具类;
  3. 静态代码块用法,将多个类成员放在一起初始化,使得程序更加规整,其中理解对象的初始化过程非常关键;
  4. 静态导包用法,将类的方法直接导入到当前类中,从而直接使用“方法名”即可调用类方法,更加方便;

第3章 操作符

​ 本章的理论内容基本上没有,主要是各种操作符,这里我们先列举下Java中的操作符种类,然后论述下平时容易被我们忽略的操作符,而那些非常常见的就省略了,比如 + - * / 这些。

操作符种类

  • 算数操作符:如,+ - * / (整数除法会直接去掉运算结果的小数位,而并非四舍五入取整) % (取模操作符,从整数除法种产生余数)

  • 自动递增和递减:如,递增++,以及递减–

  • 关系操作符:如,小于(<),大于(>),小于等于(<=),大于等于(>=),等于(==)以及不等于(!=)

  • 逻辑操作符:如,“与”(&&),“或”(||),“非”(!)

  • 直接常量:直接常量的后缀或前缀字符表示它的类型(不区分大小写)

    • 后缀:L:代表long类型
    • 后缀:D:代表double类型
    • 后缀:F:代表float类型
    • 前缀:0x:代表十六进制,后边跟 0-9 或 a-f (大小写均可)
    • 前缀:0:代表八进制,后边跟 0-7 的数字

    在Java中,二进制计数法没有直接的常量表示方法,如果想以二进制形式显示十六进制或者八进制计数法的结果时,可以通过使用 IntegerLong 类下的静态方法:toBinaryString() 来实现其他进制计数法转换为二进制计数法。注意:如果将比较小的类型传递给 Integer.toBinaryString()方法时,该类型会自动被转换成int类型。

    • 指数计数法:在Java中,1.39e-43f 代表 1.39*10^(-43)
  • 按位操作符

    ​ 按位运算符是来操作整数基本数据类型中的单个“比特”(bir),即二进制位,位运算符会对两个参数中对应的位执行布尔代数运算,并最终生成一个结果。

    ​ 位运算符来源于C语言面向底层的操作,在这种操作中经常需要直接操作硬件,设置硬件寄存器内的二进制位。Java的设计初衷是为了嵌入电视机机顶盒,所以种面向底层的操作仍被保留了下来。

    • “与”、“位与”(&)

      按位“与”操作符,如果两个数的二进制,相同位数都是1,则该位结果是1,否则是0.

      例1: 5&4

      ​ 5的二进制是 0000 0000 0000 0101

      ​ 4的二进制是 0000 0000 0000 0100

      ​ 则结果是 0000 0000 0000 0100 转为十进制是4。

    • 或”、“位或”(|)

      按位“或”操作符,如果两个数的二进制,相同位数有一个是1,则该位结果是1,否则是0

      例2: 5 | 4

      ​ 5的二进制是 0000 0000 0000 0101

      4的二进制是 0000 0000 0000 0100

      ​ 则结果是 0000 0000 0000 0101 转为十进制是5。

    • “异或、“位异或”(^)

      按位“异或”操作符,如果两个数的二进制,相同位数只有一个是1,则该位结果是1,否则是0

      例3: 5 ^ 4

      ​ 5的二进制是 0000 0000 0000 0101

      ​ 4的二进制是 0000 0000 0000 0100

      ​ 则结果是 0000 0000 0000 0001 转为十进制是1

    • “非”、“位非”(~)也称为取反操作符

      按位“非”操作符,属于一元操作符,只对一个操作数进行操作,(其他按位操作符是二元操作符)。按位“非”生成与输入位相反的值,——若输入0,则输出1,若输入1,则输出0。

      例4: ~5

      ​ 5的二进制是 0000 0000 0000 0101

      ​ 则~5是 1111 1111 1111 1010 转为十进制是 -6。

      这里出现负数,额外介绍下。
      
              电脑的的世界中只有0和1,那么负数怎么表示呢?
      
              二进制的正负是从高位看,最高位如果1则是负数,如果是0则是正数。
      
              如果负数单纯是把最高位变为1的话,在运算中会出现不是我们想要的值,所以引入了:原码,反码,补码。正数的原码,反码,补码都一样,负数的反码是对除了符号位(最高位)对原码取反,补码是对反码+1
      
              负数的二进制转化,计算机计算是用的补码
      
      1、首先取出这个数的原码的二进制,
      
      2、然后再求出反码
      
      3、最后求出补码
      
      
        例5:  -5
      
              -5的原码是                1000 0000 0000 0101
      
              求出反码的是            	1111 1111 1111 1010
      
              求出补码是                1111 1111 1111 1011
      
  • 移位操作符:是直接对二进制进行操作的一种运算符,且只能对整数进行操作!!

    • 有符号”左移:<<
      
      “有符号”右移:>>
      
      “无符号”右移:>>>
      
    • 左移运算符

      /*左移运算符<<:使指定值的所有位都左移规定的次数。
      
        1)它的通用格式如下所示:
      
        value << num
      
        num 指定要移位值value 移动的位数。
      
        左移的规则只记住一点:丢弃最高位,0补最低位
      
        如果移动的位数超过了该类型的最大位数,那么编译器会对移动的位数取模。如对int型移动33位,实际上只移动了332=1位。
      
        2)运算规则
      
        按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零。
      
        当左移的运算数是int 类型时,每移动1位它的第31位就要被移出并且丢弃;
      
        当左移的运算数是long 类型时,每移动1位它的第63位就要被移出并且丢弃。
      
        当左移的运算数是byte 和short类型时,将自动把这些类型扩大为 int 型。
      
        3)数学意义
      
        在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方
      
        4)计算过程:
      
        例如:3 <<2(3为int型)
      
        1)把3转换为二进制数字0000 0000 0000 0000 0000 0000 0000 0011,
      
        2)把该数字高位(左侧)的两个零移出,其他的数字都朝左平移2位,
      
        3)在低位(右侧)的两个空位补零。则得到的最终结果是0000 0000 0000 0000 0000 0000 0000 1100,
      
        转换为十进制是12。
      
        移动的位数超过了该类型的最大位数,
      
        如果移进高阶位(31或63位),那么该值将变为负值。下面的程序说明了这一点:
      */
      
        //Java代码
        // Left shifting as a quick way to multiply by 2.
        public class MultByTwo {
               
            public static void main(String args[]) {
               
                int i;
                int num = 0xFFFFFFE;
                for(i=0; i<4; i++) {
               
                    num = num << 1;
                    System.out.println(num);
                }
            }
        }
      
        //该程序的输出如下所示:
      
        536870908
      
        1073741816
      
        2147483632
      
        -32
      
        注:n位二进制,最高位为符号位,因此表示的数值范围-2^(n-1) ——2^(n-1) -1,所以模为2^(n-1)
    • 右移运算符

      /*右移运算符<<:使指定值的所有位都右移规定的次数。
      
        1)它的通用格式如下所示:
      
        value >> num
      
        num 指定要移位值value 移动的位数。
      
        右移的规则只记住一点:符号位不变,左边补上符号位
      
        2)运算规则:
      
        按二进制形式把所有的数字向右移动对应的位数,低位移出(舍弃),高位的空位补符号位,即正数补零,负数补1
      
        当右移的运算数是byte 和short类型时,将自动把这些类型扩大为 int 型。
      
        例如,如果要移走的值为负数,每一次右移都在左边补1,如果要移走的值为正数,每一次右移都在左边补0,这叫做符号位扩展(保留符号位)(sign extension ),在进行右移
      
        操作时用来保持负数的符号。
      
        3)数学意义
      
        右移一位相当于除2,右移n位相当于除以2的n次方。
      
        4)计算过程
      
        11 >>2(11为int型)
      
        1)11的二进制形式为:0000 0000 0000 0000 0000 0000 0000 1011
      
        2)把低位的最后两个数字移出,因为该数字是正数,所以在高位补零。
      
        3)最终结果是0000 0000 0000 0000 0000 0000 0000 0010。
      
        转换为十进制是2。
      
        35 >> 2(35为int型)
      
        35转换为二进制:0000 0000 0000 0000 0000 0000 0010 0011
      
        把低位的最后两个数字移出:0000 0000 0000 0000 0000 0000 0000 1000
      
        转换为十进制: 8
      
        5)在右移时不保留符号的出来
      
        右移后的值与0x0f进行按位与运算,这样可以舍弃任何的符号位扩展,以便得到的值可以作为定义数组的下标,从而得到对应数组元素代表的十六进制字符。
      */
      
        //例如 Java代码
        public class HexByte {
               
            public static public void main(String args[]) {
               
                char hex[] = {
               
                    '0', '1', '2', '3', '4', '5', '6', '7',
                    '8', '9', 'a', 'b', 'c', 'd', 'e', 'f''
                };
                byte b = (byte) 0xf1;
                System.out.println("b = 0x" + hex[(b >> 4) & 0x0f] + hex[b & 0x0f]);
            }
        }
      
        (b >> 4) & 0x0f的运算过程:
      
        b的二进制形式为:1111 0001
      
        4位数字被移出:1111 1111
      
        按位与运算:0000 1111
      
        转为10进制形式为:15
      
        b & 0x0f的运算过程:
      
        b的二进制形式为:1111 0001
      
        0x0f的二进制形式为:0000 1111
      
        按位与运算:0000 0001
      
        转为10进制形式为:1
      
        所以,该程序的输出如下:
      
        b = 0xf1
      
    • 无符号右移

      无符号右移运算符>>>
      
        它的通用格式如下所示:
        value >>> num
      
        num 指定要移位值value 移动的位数。
      
        无符号右移的规则只记住一点:忽略了符号位扩展,0补最高位
      
        无符号右移规则和右移运算是一样的,只是填充时不管左边的数字是正是负都用0来填充,无符号右移运算只针对负数计算,因为对于正数来说这种运算没有意义
        无符号右移运算符>>> 只是对32位和64位的值有意义
      
         位移运算符 依旧可以组合使用,例如  :  <<=  >>=  <<+  >>+  >>>= .............. 等等
      
  • 三元运算符 if-else:判断条件表达式 ? 表达式结果为true执行的代码 : 表达式结果为false执行的代码

  • **字符串操作符 + 和 += **:略

第4章 流程控制

​ 本章原书中内容比较简单,就是Java基础中的 if else 语句,for 循环,switch case 条件控制,foreach语法,break,continue,以及return,这里不再赘述。

​ 值得提一点是:return 关键字的作用:1. 指定一个方法返回什么类型的值 (如果方法返回类型是void 就不需要返回);2. 导致方法退出,即 retrun; 之后的代码不再执行。

第5章 初始化与清理

5.1 用构造器确保初始化

通过构造器,类的设计者可以确保每个对象都会得到初始化

创建对象时,如果该类具有构造器,Java会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行。例如:

class Rock{
     
    Rock(){
     		//不接受任何参数的构造器称作:默认构造器(无参构造器)
        System.out.println("Rock初始化了...")
    }
}
public class SimpleConstructor{
     
    public static void main(String[] args){
     
        for(int i=0;i<5;i++)
            new Rock();
    }
}

/*
	output:
	Rock初始化了...
	Rock初始化了...
	Rock初始化了...
	Rock初始化了...
	Rock初始化了...
*/

在创建对象时:

new Rock();

将会为该Rock对象分配存储空间,并调用响应的构造器。这样就确保在你能操作Rock对象之前,它已经恰当的初始化了。

构造器的"初始化"与"创建"在概念上是彼此独立的,然而在Java中二者是"捆绑"在一起的,两者不可分离

构造器是一种特殊类型的方法,因为它没有返回值。这与返回值为空(void)明显不同。对于空返回值,尽管方法本身不会自动返回什么,但仍可选择让它返回别的东西。构造器则不会返回任何东西,我们别无选择(虽然new表达式确实返回了对新建对象的引用,但构造器本身并没有任何返回值)。

默认构造器

如果类中没有定义构造器,则编译器会自动帮我们创建一个默认构造器 (无参构造器)。如果类中已经定义了一个构造器(无论是否有参数),编译器就不会再帮你创建默认构造器。

5.2 方法重载

5.2.1 方法重载

方法重载概念

如果一个类中有多个具有相同名称但参数不同的方法,则称为方法重载。如果只需要执行一个操作,具有相同的方法名称将增加程序的可读性。
假设必须执行给定数值的添加操作(求和),但是参数的数量不固定,如果为两个参数编写add1(int,int)方法,为三个参数编写add2(int,int,int)方法.可以用重载:

两个参数相加

int add(int num1,int num2){
     
	return num1 + num2;
}

三个参数相加

int add(int num1,int num2 ,int num3){
     
	return num1 + num2 + num3;
}

无数个参数相加

static int add(int... args) {
     
		int result = 0;
		for(int i = 0;i < args.length;i ++) {
     
			result += args[i];
		}
		return result;
}

测试例子

public static void main(String[] args) {
     
	System.out.println(add(465465,465465,31,36465,41,31,465,41,3,1654,654,1,32,165,465,4,3213,246,54,65,465454654));
	System.out.println(add(1,2));
	System.out.println(add(1,2,3));
}

static int add(int... nums) {
     
	int result = 0;
	for(int i = 0;i < nums.length;i ++) {
     
		result += nums[i];
	}
	return result;
}

static int add(int num1,int num2) {
     
	return num1 + num2;
}

static int add(int num1,int num2,int num3) {
     
	return num1 +num2 + num3;
}

/*
	output结果:
	466429214
	3
	6
*/

为什么要方法重载?

方法重载可以提高可读性。如果没有重载,两个参数的add(int num1,int num2)存在后,就不能存在三个参数的add(int num1,int num2,int num3),或者说三个参数的add方法必须改名,也就是add1(int num1,int num2,int num3);而重载可以解决以上问题。

方法重载的不同方式

  • 通过改变参数的数量

    add(int num1,int num2){
           }
    add(int num1,int num2,int num3){
           }
    add(int... args){
           }
    
  • 通过改变参数类型

    add(int num1,int num2){
           }
    add( num1,double num2){
           }
    

注意:方法重载与返回类型无关

例如:

class Add(){
     
	static int add(int num1,int num2){
     
		return num1 + num2;
	}
	static double add(int num1,int num2){
     
		return num1 + num2;
	}

	public static void main(String args){
     
		add(1,2);
	}
}

以上函数当调用add的时候,该找哪个函数呢?是int add(){} 还是double add(){}?所以根据返回数据类型来重载,是很容易造成混淆,因为不知道调用哪个返回类型的add方法。

在这里,编译时错误优于运行时错误。 所以,如果你声明相同的方法具有相同的参数,java编译器就不知道执行哪个方法,造成不确定的错误

main() 方法可以重载吗?

main()方法也是普通方法,可以重载,只不过虚拟机只调用带字符串公共类型的方法,public static void main(String[] args){},但是你可以自定义你用的main()方法,main(int a);main(String a);

class TestOverloading{
       
    public static void main(String[] args){
     
        System.out.println("main with String[]");
    }  
    public static void main(String args){
     
        System.out.println("main with String");
    }  
    public static void main(){
     
        System.out.println("main without args");
    }  
}

输出结果:

main with String[]

虚拟机只执行一个main方法。
如果你在虚拟机执行的main方法中调用其他方法

class TestOverloading{
       
    public static void main(String[] args){
     
        System.out.println("main with String[]");
        main("234");
        main();
    }  
    public static void main(String args){
     
        System.out.println("main with String");
    }  
    public static void main(){
     
        System.out.println("main without args");
    }  
}

输出结果:

main with String[];
main with String;
main without args;

5.2.2 方法重载与类型提升

概念

如果没有找到匹配的数据类型,那么Java会隐式地将一个类型提升到另一个类型。 让我们通过下面的图示来理解这个概念:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XHz93dQg-1604286999322)(https://github.com/CodeFarmer1999/img_cloud/blob/master/img/image-20200601140038276.png?raw=true)]

其中8字节的long可以自动整型提升为4字节的float,且4字节的float的最大值大于long的最大值,float的最小值小于long的最小值

例子:

public class LongToFloat {
     

	public static Logger log = Logger.getLogger(LongToFloat.class.getName());
	
	public static void main(String[] args) {
     
		final long MAX_VALUE_TO_LONG = Long.MAX_VALUE;
		final long MIN_VALUE_TO_LONG = Long.MIN_VALUE;
		final float MAX_VALUE_TO_FLOAT = Float.MAX_VALUE;
		final float MIN_VALUE_TO_FLOAT = Float.MIN_VALUE;          //正数的最小值
		float NEGATIVE_MIN_VALUE_TO_FLOAT = -1 * MAX_VALUE_TO_FLOAT - 1.01E31f;
		log.info("\n" + MAX_VALUE_TO_FLOAT
				+ "\n" + MIN_VALUE_TO_FLOAT + "(float正数的最小值)"
				+ "\n" + MAX_VALUE_TO_LONG
				+ "\n" + MIN_VALUE_TO_LONG
				+ "\nfloat最大的数 - long最大的数:" + (MAX_VALUE_TO_FLOAT - MAX_VALUE_TO_LONG)
				+ "\n" + NEGATIVE_MIN_VALUE_TO_FLOAT);
		float longToFloatMax = MAX_VALUE_TO_LONG;                                                   //long最大值可以自动整型提升转换成float
		log.info("\n把long最大值转换成float"
				+ "\n\t转换前:" 
				+ MAX_VALUE_TO_LONG
				+ "\n\t转换后"
				+ longToFloatMax);
//		long floatToLongMax = MAX_VALUE_TO_FLOAT;      编译报错,float不能转换long            
//		long floatToLongMin = MIN_VALUE_TO_FLOAT;      编译报错,float不能转换long
	}

}

输出结果为:

3.4028235E38                                                                        //float正数最大值
1.4E-45(float正数的最小值)                        //float正数最小值
9223372036854775807                            //long最大值
-9223372036854775808                           //long最小值
float最大的数 - long最大的数:3.4028235E38        //float最大值比long最大值大的多
-3.4028235E38                                 //大约计算后的float的最小值

把long最大值转换成float
	转换前:9223372036854775807
	转换后9.223372E18                          //long可以不丢失原因精确转换成float;

具体原因请移步float和double类型的范围和精度。

原书中例子

import static net.mindview.util.Print.*;

public class PrimitiveOverloading {
     
  void f1(char x) {
      printnb("f1(char) "); }
  void f1(byte x) {
      printnb("f1(byte) "); }
  void f1(short x) {
      printnb("f1(short) "); }
  void f1(int x) {
      printnb("f1(int) "); }
  void f1(long x) {
      printnb("f1(long) "); }
  void f1(float x) {
      printnb("f1(float) "); }
  void f1(double x) {
      printnb("f1(double) "); }

  void f2(byte x) {
      printnb("f2(byte) "); }
  void f2(short x) {
      printnb("f2(short) "); }
  void f2(int x) {
      printnb("f2(int) "); }
  void f2(long x) {
      printnb("f2(long) "); }
  void f2(float x) {
      printnb("f2(float) "); }
  void f2(double x) {
      printnb("f2(double) "); }

  void f3(short x) {
      printnb("f3(short) "); }
  void f3(int x) {
      printnb("f3(int) "); }
  void f3(long x) {
      printnb("f3(long) "); }
  void f3(float x) {
      printnb("f3(float) "); }
  void f3(double x) {
      printnb("f3(double) "); }

  void f4(int x) {
      printnb("f4(int) "); }
  void f4(long x) {
      printnb("f4(long) "); }
  void f4(float x) {
      printnb("f4(float) "); }
  void f4(double x) {
      printnb("f4(double) "); }

  void f5(long x) {
      printnb("f5(long) "); }
  void f5(float x) {
      printnb("f5(float) "); }
  void f5(double x) {
      printnb("f5(double) "); }

  void f6(float x) {
      printnb("f6(float) "); }
  void f6(double x) {
      printnb("f6(double) "); }

  void f7(double x) {
      printnb("f7(double) "); }

  void testConstVal() {
     
    printnb("5: ");
    f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5); print();
  }
  void testChar() {
     
    char x = 'x';
    printnb("char: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testByte() {
     
    byte x = 0;
    printnb("byte: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testShort() {
     
    short x = 0;
    printnb("short: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testInt() {
     
    int x = 0;
    printnb("int: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testLong() {
     
    long x = 0;
    printnb("long: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testFloat() {
     
    float x = 0;
    printnb("float: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testDouble() {
     
    double x = 0;
    printnb("double: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  public static void main(String[] args) {
     
    PrimitiveOverloading p =
      new PrimitiveOverloading();
    p.testConstVal();
    p.testChar();
    p.testByte();
    p.testShort();
    p.testInt();
    p.testLong();
    p.testFloat();
    p.testDouble();
  }
} 
/* Output:
5: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
char: f1(char) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
byte: f1(byte) f2(byte) f3(short) f4(int) f5(long) f6(float) f7(double)
short: f1(short) f2(short) f3(short) f4(int) f5(long) f6(float) f7(double)
int: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
long: f1(long) f2(long) f3(long) f4(long) f5(long) f6(float) f7(double)
float: f1(float) f2(float) f3(float) f4(float) f5(float) f6(float) f7(double)
double: f1(double) f2(double) f3(double) f4(double) f5(double) f6(double) f7(double)
*///:~

从原书例子中可以发现,常数值5被当做int值处理所以如果有某个重载方法接收int类型参数,它就会被调用。

对于其他情况:

  • 如果传入的数据类型 (实际参数类型) 小于方法中声明的形式参数类型,实际数据类型就会提升。char类型略有不同,如果无法找到恰好接收char参数的方法,就会把char直接提升至int类型。

  • 如果传入的数据类型 (实际参数类型) 大于方法中声明的形式参数类型,就得通过类型转换来执行窄化转换,如果不这样做,编译就会报错。

    如:

    import static net.mindview.util.Print.*;
    
    public class Demotion {
           
      void f1(char x) {
            print("f1(char)"); }
      void f1(byte x) {
            print("f1(byte)"); }
      void f1(short x) {
            print("f1(short)"); }
      void f1(int x) {
            print("f1(int)"); }
      void f1(long x) {
            print("f1(long)"); }
      void f1(float x) {
            print("f1(float)"); }
      void f1(double x) {
            print("f1(double)"); }
    
      void f2(char x) {
            print("f2(char)"); }
      void f2(byte x) {
            print("f2(byte)"); }
      void f2(short x) {
            print("f2(short)"); }
      void f2(int x) {
            print("f2(int)"); }
      void f2(long x) {
            print("f2(long)"); }
      void f2(float x) {
            print("f2(float)"); }
    
      void f3(char x) {
            print("f3(char)"); }
      void f3(byte x) {
            print("f3(byte)"); }
      void f3(short x) {
            print("f3(short)"); }
      void f3(int x) {
            print("f3(int)"); }
      void f3(long x) {
            print("f3(long)"); }
    
      void f4(char x) {
            print("f4(char)"); }
      void f4(byte x) {
            print("f4(byte)"); }
      void f4(short x) {
            print("f4(short)"); }
      void f4(int x) {
            print("f4(int)"); }
    
      void f5(char x) {
            print("f5(char)"); }
      void f5(byte x) {
            print("f5(byte)"); }
      void f5(short x) {
            print("f5(short)"); }
    
      void f6(char x) {
            print("f6(char)"); }
      void f6(byte x) {
            print("f6(byte)"); }
    
      void f7(char x) {
            print("f7(char)"); }
    
      void testDouble() {
           
        double x = 0;
        print("double argument:");
        f1(x);f2((float)x);f3((long)x);f4((int)x);
        f5((short)x);f6((byte)x);f7((char)x);
      }
      public static void main(String[] args) {
           
        Demotion p = new Demotion();
        p.testDouble();
      }
    } 
    /* Output:
    double argument:
    f1(double)
    f2(float)
    f3(long)
    f4(int)
    f5(short)
    f6(byte)
    f7(char)
    *///:~
    

5.3 this 关键字

this关键字只能在方法内部使用,共有四种用法

(1)this调用本类中的属性,也就是类中的成员变量。

(2)this调用本类中的其他方法。

(3)this调用本类中的其他构造方法,调用时要放在构造方法的首行

(4)this表示当前对象本身。

1. this调用成员变量

使用this调用成员变量发生在方法内部,当成员变量(类下声明的变量)的名字与局部变量(方法内声明的变量)的名字相同时,使用this.变量名来表示该变量代表的是成员变量。

//声明成员变量
private String name;
//参数中的变量是方法中的局部变量
public void setName(String name){
     
    //this.name表示的就是成员变量,name表示局部变量
    this.name = name;
}

2. this调用成员方法

使用this调用成员方法同样也发生在方法内部。通常情况下,我们在方法内部调用其他方法时,使用的方式是直接调用,即直接写方法名。其实,此时我们可以在要被调用的方法名前使用this关键字加“.”调用,这两种调用成员方法的方式是一样的,由于使用第二种方式没有多大的意义,所以一般情况下,我们选择直接调用的方式。

public class ThisTest{
     
    public void method1(){
     
        System.out.println("成员方法");
    }
    public void method2(){
     
        //以下两种调用方法的方式效果一致
        method1();
        this.method1();
    }
}

3. this调用其他重载的构造器

使用this关键字调用其他重载的构造器是this关键字非常重要的用法之一,采用this调用其他构造方法需要将this语句写在构造器内的第一行,否则会报错。这种调用方式的优点在于一个构造器可以不必重复编写其他构造器中已有的代码,而是通过调用其他构造器实现代码复用,从而提供良好的类代码结构。

public class ThisTest{
     
    //定义两个成员变量
    private String name;
    private int height;
    //无参构造器
    public ThisTest(){
     
        //使用this调用参数为String类型的构造器
        this("Ben");
    }
    //参数为String类型的构造器
    public ThisTest(String name){
     
        //使用this调用参数为String和int类型的构造器
        this(name,165);
    }
    //参数为String和int类型的构造器
    public ThisTest(String name, int height){
     
        //为两个属性赋值
        this.name = name;
        this.height = height;
    }
}
public class Test(){
     
    public static void main(String[] args){
     
        //创建对象
        new ThisTest();
    }
}

创建对象时,构造器就会被调用,我们来看一下构造器的调用顺序:
首先,创建对象时没有传入参数,所以先调用无参构造器,进入无参构造器后,执行this(“Ben”),然后进入参数为String类型的有参构造器,此时将”Ben”传给name,执行this(name,165),最后进入参数为String和int类型的构造器,执行该构造器内的语句,即最终的name为”Ben”,最终的height为165。
这种使用参数最多的构造器来初始化成员变量的方式在开发中很常用。

2. this表示当前对象

直接上代码

public class This2Test{
     
    //定义一个属性
    public String name;
    //参数为String类型的构造器
    public This2Test(String name){
     
        this.name = name;
    }
    //定义一个参数为引用类型的方法
    public void method3(This2Test tt){
     
        //输出该对象的属性
        System.out.println(tt.name);
    }
    public void method4(){
     
        //调用方法并传入this
        method3(this);
    }
}
public class Test{
     
    public static void main(String[] args){
     
        //创建对象t
        This2Test t = new This2Test("贝克")//对象t调用方法
        t.method4();
    }
}
/*
    输出结果为:
    贝克
*

我们来分析一下运行程序是怎么运行的,传入的this代表的是什么呢?为什么输出的是贝克呢?
首先看程序的入口main方法,先创建对象t,创建对象时,进入Test2This类调用它的有参构造器,此时name为传入的”贝克”,也就是说当前创建的这个对象t的name为”贝克”。然后,用对象t去调用method4()方法,该方法内部调用了method3()方法,并传入了this。因此,进入method3()方法,打印this的名字,这里的这个this代表的就是调用method4()方法的对象,即一开始的对象t。所以这里就体现了this的第四种用法————this表示当前对象。在这里,可以简单理解为:this在method4()方法中被调用,因此this指的就是调用method4()这个方法的对象。

5.4 清理:垃圾回收

垃圾回收机制详细内容,读者可以从深入理解Java虚拟机这本书中学习了解,在本小结只做简单概述!

垃圾回收器如何工作?

在程序语言中,由于在堆上分配对象的代价十分高昂,因此我们可能会觉得Java中所有对象 (基本类型除外) 都在堆上分配的方式也十分高昂。然而,垃圾回收器对于提高对象的创建速度,是具有明显效果的!这是因为——存储空间的释放会影响存储空间的分配效率

垃圾回收器工作时,一边回收空间,一边使堆中的对象紧凑排列,这样 “堆指针” 就可以很容易移动到更靠近传送带的开始处,也就尽力避免了一些必要的错误。通过垃圾回收器对对象重新排列,实现了一种高速的、有无限空间可供分配的堆模型。

5.5 构造器初始化

5.5.1 初始化顺序

类的内部,变量定义的先后顺序决定了初始化的顺序即使变量定义散布于方法定义之间,它们仍旧会在任何方法 (包括构造器) 被调用之前得到初始化。例如书中例子:

import static net.mindview.util.Print.*;

class Window {
     
  Window(int marker) {
      print("Window(" + marker + ")"); }
}

class House {
     
  Window w1 = new Window(1); // Windows对象的定义于House构造器之前
  House() {
     
    // 在构造器的内部
    print("House()");
    w3 = new Window(33); // 重新初始化Windows对象的w3实例
  }
  Window w2 = new Window(2); // Windows对象的定义于House构造器之后
  void f() {
      print("f()"); }
  Window w3 = new Window(3); // Windows对象的定义于House类的最后位置
}

public class OrderOfInitialization {
     
  public static void main(String[] args) {
     
    House h = new House();
    h.f(); // 显示构造器已经被执行过
  }
} 
/* Output:
    Window(1)
    Window(2)
    Window(3)
    House()
    Window(33)
    f()
*///:~

在Hourse类中,故意把几个Windows对象的定义散布在Hourse类的不同位置,来证明它们全都会在调用构造器或其他方法之前得到初始化。此外w3实例在House构造器内部重新被初始化。

由输出结果可得结论:即使变量定义散布于方法定义之间,它们仍旧会在任何方法 (包括构造器) 被调用之前得到初始化

5.5.2 静态数据的初始化

无论创建多少个对象,静态数据都只占用一份存储区域static关键字不能应用于局部变量,因此它只能用作于。如果一个域是静态的基本类型域,且也没有对它进行初始化,那么它就会获得基本类型的标准初始值;而如果它是一个对象引用,那么它的默认初始值就是null。

想进一步了解静态存储区域是如何初始化的,仔细分析该例子:

import static net.mindview.util.Print.*;

class Bowl {
     
  Bowl(int marker) {
     
    print("Bowl(" + marker + ")");
  }
  void f1(int marker) {
     
    print("f1(" + marker + ")");
  }
}

class Table {
     
  static Bowl bowl1 = new Bowl(1);
  Table() {
     
    print("Table()");
    bowl2.f1(1);
  }
  void f2(int marker) {
     
    print("f2(" + marker + ")");
  }
  static Bowl bowl2 = new Bowl(2);
}

class Cupboard {
     
  Bowl bowl3 = new Bowl(3);
  static Bowl bowl4 = new Bowl(4);
  Cupboard() {
     
    print("Cupboard()");
    bowl4.f1(2);
  }
  void f3(int marker) {
     
    print("f3(" + marker + ")");
  }
  static Bowl bowl5 = new Bowl(5);
}

public class StaticInitialization {
     
  public static void main(String[] args) {
     
    print("Creating new Cupboard() in main");
    new Cupboard();
    print("Creating new Cupboard() in main");
    new Cupboard();
    table.f2(1);
    cupboard.f3(1);
  }
  static Table table = new Table();
  static Cupboard cupboard = new Cupboard();
} 
/* Output:
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)
*///:~

执行顺序分析图:

Java编程思想(第4版本)1-15章笔记_第4张图片

分析输出结果可知:

静态初始化只有在必要时刻才会进行。如果不创建Table对象,也不引用Table.b1或Table.b2,那么静态的Bowl b1和b2永远都不会被创建。只有第一个Table对象被创建(或者第一次访问静态数据)的时候,它们才会被初始化。此后,静态对象不会再次进行初始化。

初始化的顺序是:先静态对象,然后是"非静态"对象。

5.5.3 静态代码块

静态代码块:执行优先级高于非静态的初始化块,它会在类初始化的时候执行一次,执行完成便销毁,它仅能初始化类变量,即static修饰的数据成员。简单来说就是:静态代码块随着类的加载而执行,而且只执行一次

5.5.4 非静态代码块

执行的时候如果有静态初始化块,先执行静态初始化块再执行非静态初始化块,在每个对象生成时都会被执行一次,换句话来说就是:非静态代码块会随着类的加载而执行,每加载一次,都会执行一次,它可以初始化类的实例变量。非静态初始化块会在构造函数执行时,在构造函数主体代码执行之前被运行

代码块的执行顺序静态代码块----->非静态代码块-------->构造函数

关于静态代码块、非静态代码块、构造函数的执行顺序实例,在前面的章节已经给出

请参考:第1章 对象导论的1.5.3 实例扩展 这一小节的实例。

5.6 数组初始化

数组的创建与使用

int[] arr1;	//方式一

int arr2[];	//方式二

Java中编译器不允许指定数组的大小。上述两种方式定义的事实上只是数组的一个引用(为数组的引用分配了存储空间),而没有给数组对象本身分配任何存储空间。

想要给数组创建响相应的存储空间,就需要写初始化表达式。如:

int[] arr1 = {
     1,2,3,4,5};

这种情况下,存储空间的分配(等价于使用new)将由编译器负责。

我们用方式一,定义了数组的引用,因此可以通过该引用,将数组arr1的内容赋值给另一个数组,如:

import static net.mindview.util.Print.*;

public class ArraysOfPrimitives {
     
  public static void main(String[] args) {
     
    int[] arr1 = {
      1, 2, 3, 4, 5 };
    int[] arr2;
    arr2 = arr1;
    for(int i = 0; i < arr2.length; i++)
      arr2[i] = arr2[i] + 1;
    for(int i = 0; i < arr1.length; i++)
      print("arr1[" + i + "] = " + arr1[i]);
  }
} 
/* Output:
a1[0] = 2
a1[1] = 3
a1[2] = 4
a1[3] = 5
a1[4] = 6
*///:~

我们也可以直接用new的方式在数组里创建元素,这种方式创建的是基本类型数组,如:

import java.util.*;
import static net.mindview.util.Print.*;

public class ArrayNew {
     
  public static void main(String[] args) {
     
    int[] a;
    Random rand = new Random(47);
    a = new int[rand.nextInt(20)];
    print("length of a = " + a.length);
    print(Arrays.toString(a));
  }
} 
/* Output:
length of a = 18
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
*///:~

有输出结果可知,数组元素中的基本数据类型值会自动初始化为空值(对于数组和字符就是0,对于布尔型就是false)。

这里给一个例子:创建一个String对象数组,将其传递给另一个main()方法,以提供参数,用来替换传递给该main()方法的命令行参数,如:

public class DynamicArray {
     
  public static void main(String[] args) {
     
    Other.main(new String[]{
      "fiddle", "de", "dum" });
  }
}

class Other {
     
  public static void main(String[] args) {
     
    for(String s : args)
      System.out.print(s + " ");
  }
} 
/* Output:
fiddle de dum
*///:~

有结果可知:为Other.main()的参数而创建的数组是在方法调用处创建的,因此我们甚至可以在调用时候提供可替换的参数。

可变参数列表

这里不再详细讨论,直接上代码供大家参考。

class A {
     }

public class VarArgs {
     
  static void printArray(Object[] args) {
     
    for(Object obj : args)
      System.out.print(obj + " ");
    System.out.println();
  }
  public static void main(String[] args) {
     
    printArray(new Object[]{
     
      new Integer(47), new Float(3.14), new Double(11.11)
    });
    printArray(new Object[]{
     "one", "two", "three" });
    printArray(new Object[]{
     new A(), new A(), new A()});
  }
} 
/* Output: (Sample)
47 3.14 11.11
one two three
A@1a46e30 A@3e25a5 A@19821f
*///:~

由于未定义toString()方法,所以这里输出的 A@1a46e30 A@3e25a5 A@19821f 是:类的名字和对象的地址。

格式是:类的名称+@+多个十六进制数字。

5.7 枚举类型

定义枚举:

public enum Spiciness {
     
  NOT, MILD, MEDIUM, HOT, FLAMING
}

在switch case 中使用枚举:

public class Burrito {
     
  Spiciness degree;
  public Burrito(Spiciness degree) {
      this.degree = degree;}
  public void describe() {
     
    System.out.print("This burrito is ");
    switch(degree) {
     
      case NOT:    System.out.println("not spicy at all.");
                   break;
      case MILD:
      case MEDIUM: System.out.println("a little hot.");
                   break;
      case HOT:
      case FLAMING:
      default:     System.out.println("maybe too hot.");
    }
  }	
  public static void main(String[] args) {
     
    Burrito
      plain = new Burrito(Spiciness.NOT),
      greenChile = new Burrito(Spiciness.MEDIUM),
      jalapeno = new Burrito(Spiciness.HOT);
    plain.describe();
    greenChile.describe();
    jalapeno.describe();
  }
} 
/* Output:
This burrito is not spicy at all.
This burrito is a little hot.
This burrito is maybe too hot.
*///:~

:第6章 访问权限控制,主要讲解四种访问权限修饰符public、default、protected、private有Java基础的应该很容易理解,这里简单总结下,不再详细论述。

  • private(私有的):只有在同一个类才能访问。内部类、成员变量、方法;
  • default/friendly(默认的):同一个包中可以访问,其他包中不能访问。类、成员变量、方法、接口;
  • protected(受保护的):同一个包中可以访问,不同的包需要用子类来调用。内部类、成员变量、方法;
  • public(公共的):可以被任何对象访问。类成员变量、方法;

对比:

同类 同包 不同包子类 不同包非子类
public 1 1 1 1
protected 1 1 1 0
default 1 1 0 0
private 1 0 0 0

5.8 final 关键字

Java中的final关键字的含义是指:无法改变的,不想做出改变可能出于两种理由:设计或效率。可能用到final的三种情况:数据、方法、类

final 的使用

final 属性或对象的引用

final属性应用于:

  • 一个永远不改变的编译时常量
  • 一个在运行时被初始化的值,且不希望它被改变

此属性就是一个常量,一旦初始后,不可再被赋值。习惯上,重用大写字符表示。此常量在哪里赋值:

①此常量不能使用默认初始

②可以显示的赋值、代码块、构造器

注:显示赋值和代码块赋值只能有其中的一个,变量用static final修饰:全局常量,如Math类的PI。

一个既是static又是final的域只占据一段不能被改变的存储空间

  • 当对象的引用使用final关键字时,使该对象的引用恒定不变,该引用一旦初始化指向某个对象,就无法再把它指向另一个对象。然而,对象自身是可以被修改的。这一限制同样适用于数组,它也是对象。

  • 当基本类型使用final关键字时,使该类型数值恒定不变

既是static又是final的域(即编译期常量)将用大写表示,并使用下划线分割各个单词。

书中例子:

import java.util.*;
import static net.mindview.util.Print.*;

class Value {
     
  int i; // Package access
  public Value(int i) {
      this.i = i; }
}

public class FinalData {
     
  private static Random rand = new Random(47);
  private String id;
  public FinalData(String id) {
      this.id = id; }
  // Can be compile-time constants:
  private final int valueOne = 9;
  private static final int VALUE_TWO = 99;
  // Typical public constant:
  public static final int VALUE_THREE = 39;
  // Cannot be compile-time constants:
  private final int i4 = rand.nextInt(20);
  static final int INT_5 = rand.nextInt(20);
  private Value v1 = new Value(11);
  private final Value v2 = new Value(22);
  private static final Value VAL_3 = new Value(33);
  // Arrays:
  private final int[] a = {
      1, 2, 3, 4, 5, 6 };
  public String toString() {
     
    return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
  }
  public static void main(String[] args) {
     
    FinalData fd1 = new FinalData("fd1");
    //! fd1.valueOne++; // Error: can't change value
    fd1.v2.i++; // Object isn't constant!
    fd1.v1 = new Value(9); // OK -- not final
    for(int i = 0; i < fd1.a.length; i++)
      fd1.a[i]++; // Object isn't constant!
    //! fd1.v2 = new Value(0); // Error: Can't
    //! fd1.VAL_3 = new Value(1); // change reference
    //! fd1.a = new int[3];
    print(fd1);
    print("Creating new FinalData");
    FinalData fd2 = new FinalData("fd2");
    print(fd1);
    print(fd2);
  }
} 
/* Output:
fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18
*///:~

空白 final

所谓空白final就是指被final关键字修饰,但又未给定初值的域。空白final在使用上提供了更大的灵活性,为此一个类中的final域就可以根据对象而有所不同,却又保持其恒定不变的特性。

比如:

class Poppet {
     
  private int i;
  Poppet(int ii) {
      i = ii; }
}

public class BlankFinal {
     
  private final int i = 0; // Initialized final
  private final int j; // Blank final
  private final Poppet p; // Blank final reference
  // Blank finals MUST be initialized in the constructor:
  public BlankFinal() {
     
    j = 1; // Initialize blank final
    p = new Poppet(1); // Initialize blank final reference
  }
  public BlankFinal(int x) {
     
    j = x; // Initialize blank final
    p = new Poppet(x); // Initialize blank final reference
  }
  public static void main(String[] args) {
     
    new BlankFinal();
    new BlankFinal(47);
  }
} ///:~

必须在域的定义处或者每个构造器中用表达式对final进行赋值。

final 参数

Java中允许在参数列表中以声明的方式将参数指明为final。这意味着你无法在方法中更改参数引用所指向的对象:

class Gizmo {
     
  public void spin() {
     }
}

public class FinalArguments {
     
  void with(final Gizmo g) {
     
    //! g = new Gizmo(); // Illegal -- g is final
  }
  void without(Gizmo g) {
     
    g = new Gizmo(); // OK -- g not final
    g.spin();
  }
  // void f(final int i) { i++; } // Can't change
  // You can only read from a final primitive:
  int g(final int i) {
      return i + 1; }
  public static void main(String[] args) {
     
    FinalArguments bf = new FinalArguments();
    bf.without(null);
    bf.with(null);
  }
} ///:~

方法f()g()展示了基本类型的参数被指明为final时所出现的结果:我们可以读取参数,但却无法修改参数。这一特性主要用来向匿名内部类传递数据,在第十章 中会详细介绍。

final 方法

使用**final **方法的原因有两个:

  • 把方法锁定,以防止任何继承类修改它的含义,确保在继承中使方法行为保持不变,并且不会被覆盖。
  • 过去在老版本的Java中final方法一定程度上会提升效率(随着JVM虚拟机优化,这种方式渐渐可被忽略)。

如:Object的getClass();功能已经确定下来,就算子类重写了该方法后,也是实现一样的功能,所以没有必要被子类重写。

final 和 private 关键字

类中所有的private方法都会隐式地指定被final修饰

final 类

当某个类被final修饰时,就意味着该类不打算被继承,即不可以有子类。

final修饰类:(俗称太监类不能有孩子),提高安全性,提高程序的可读性,这个类就不能被继承。如:String类、StringBuffer类、System类。

final常见面试题

/*
 * 错误,调用方法的时候已经对x进行赋值,不能再对其进行++操作。
 */
public class Something {
      
	public int addOne(final int x) {
      
		return ++x; 
	}  
}
/*
 * 正确,因为final修饰的类,但++的却是其对象,说明类属性改变,类不一定跟着改变
 */
public class Something {
      
	public static void main(String[] args) {
      
		Other o = new Other(); 
		new Something().addOne(o); 
	} 
	public void addOne(final Other o) {
      
		o.i++; 
	}  
} 
class Other {
      
	public int i;
} 
/*
 * 问题:使用final关键字修饰一个变量时,是引用不能变,还是引用的对象不能变
 * 答:
 * 使用final关键字修饰一个变量时,是指引用变量不能变,引用变量所指向的对象中的内容还是可以改变的。
 */
public class Test10 {
     
	// final修饰基本类型的变量
	public static final char CHAR = '中';
	// final修饰引用类型的变量
	public static final StringBuffer a = new StringBuffer("StringBuffer");
 
	public static void main(String[] args) {
     
		// 编译报错,引用不能变
		// a = new StringBuffer("hehe");
		// 引用变量所指向的对象中的内容还是可以改变的
		a.append("xxx");
 
	}
 
	public static int method1(final int i) {
     
		// i = i + 1;// 编译报错,因为final修饰的是基本类型的变量
		return i;
	}
 
	// 有人在定义方法的参数(引用变量)时,可能想采用如下的形式来阻止方法内部修改传进来的参数对象,
	// 实际上,这是办不到的,在该方法内部仍然可以增加如下代码来修改参数对象
	public static void method2(final StringBuffer buffer) {
     
		buffer.append("buffer");// 编译通过,因为final修饰的是引用类型的变量
	}
 
}

第6章 访问权限修饰符 第7章 复用类 主要介绍继承,继承内容已在第1章1.5小结介绍过,此章节省略

同理,第8章 多态、第9章 接口不再重复论述,参考第1章1.6、1.9小结

第10章 内部类

引言:内部类,即将一个类的定义放在另一个类的定义内部。内部类与组合是完全不同的概念。内部类看似是一种代码的隐藏机制,其实,它能够了解外部类,并且与之通信,这为我们的编程提供了极大的方便。

10.1 内部类的定义

内部类: 所谓内部类就是在一个类内部进行其他类结构的嵌套操作。

class Outer{
     
    private String str ="外部类中的字符串";
    //************************** 
    //定义一个内部类
    class Inner{
     
        private String inStr= "内部类中的字符串";
        //定义一个普通方法
        public void print(){
     
            //调用外部类的str属性
            System.out.println(str);
        }
    }
    //************************** 
    //在外部类中定义一个方法,该方法负责产生内部类对象并调用print()方法
    public void fun(){
     
        //内部类对象
        Inner in = new Inner();
        //内部类对象提供的print
        in.print();
    }
}
public class Test{
     
    public static void main(String[] args)
    {
     
        //创建外部类对象
        Outer out = new Outer();
        //外部类方法
        out.fun();
    }
}
/*  output:
	外部类中的字符串
*/

但是如果去掉内部类:

class Outer{
     
    private String outStr ="Outer中的字符串";
    public String getOutStr()
    {
     
        return outStr;
    }
    public void fun(){
       //2
        //this表示当前对象
        Inner in = new Inner(this); //3
        in.print();                 //5
    }
}
class Inner{
     
    private String inStr= "Inner中的字符串";
    private Outer out;
    //构造注入
    public Inner(Outer out)  //3
    {
     
        this.out=out;       //4.为Inner中的out变量初始化
    }
    public void print(){
         //6
        System.out.println(out.getOutStr()); //7
    }
} 
public class Test{
     
    public static void main(String[] args)
    {
     
        Outer out = new Outer();  //1.
        out.fun(); //2.
    }
}
/*  output:
	Outer中的字符串
*/

去掉内部类之后发现程序更加难以理解。

10.2 内部类的优缺点

内部类的优点:

  • 内部类与外部类可以方便的访问彼此的私有域(包括私有方法、私有属性)。
  • 内部类是另外一种封装,对外部的其他类隐藏。
  • 内部类可以实现java的单继承局限。

内部类的缺点:

  • 结构复杂。

记录:使用内部类实现多继承:

class A {
     
    private String name = "A类的私有域";
    public String getName() {
     
        return name;
    }
}
class B {
     
    private int age = 20;
    public int getAge() {
     
        return age;
    }
}
class Outter {
     
    private class InnerClassA extends A {
     
        public String name() {
     
            return super.getName();//得到父类A的name私有属性
    }
}
    private class InnerClassB extends B {
     
        public int age() {
     
            return super.getAge();
    }
}
    public String name() {
     
        return new InnerClassA().name();
    }
    public int age() {
     
        return new InnerClassB().age();
    }
}
public class Test2 {
     
        public static void main(String[] args) {
     
            Outter outter = new Outter();
            System.out.println(outter.name());
            System.out.println(outter.age());
        }
}
/*	output:
    A类的私有域
    20
*/

10.3 创建内部类

在外部类外部 创建非静态内部类

语法: 外部类.内部类 内部类对象 = new 外部类().new 内部类();
举例: Outer.Inner in = new Outer().new Inner();

在外部类外部 创建静态内部类

语法: 外部类.内部类 内部类对象 = new 外部类.内部类();
举例: Outer.Inner in = new Outer.Inner();

在外部类内部创建内部类语法

在外部类内部创建内部类,就像普通对象一样直接创建:Inner in = new Inner();

书中例子:

public class Parcel2 {
     
    
	class Contents{
     
		private int i=11;
		public int value(){
     
			return i;
		}
	}
	
	class Destination{
     
		private String label;
		public Destination(String whereTo) {
     
			label=whereTo;
		}
		String readLabel(){
     
			return label;
		}
	}
	
	public Contents contents(){
     //外部类的方法,返回一个内部类对象
		return new Contents();
	}
	
	public void ship(String dest){
     
		Contents c=new Contents();
		Destination d=new Destination(dest);
		System.out.println(d.readLabel());
	}
	
	public static void main(String[] args){
     
		Parcel2 p=new Parcel2();
		p.ship("Tasmania");
		Parcel2 q=new Parcel2();
		Parcel2.Contents c=q.contents();//利用外部类的对象访问外部类方法,返回内部类的对象
	}
}
/*Output:
Tasmania
*/

从main中可以看出,我们从外部类的非静态方法中获取了内部类对象,所以必须指明这个对象的类型:OuterClassName.InnerClassName

10.4 链接到外部类

从main中,使用内部类最重要的一点就是,生成的内部类对象可以与制造它的外部类对象进行通信,它可以访问外部类对象的所有成员和外围类的所有元素,不需要任何条件。

interface Selector{
     
	boolean end();
	Object current();
	void next();
}
 
public class Sequence {
     
	private Object[] items;
	private int next=0;
	public Sequence(int size){
     //固定数组大小
		items=new Object[size];
	}
	public void add(Object x){
     //在末尾添加新的Object
		if(next<items.length)
			items[next++]=x;
	}
	private class SequenceSelector implements Selector{
     //内部类实现Selector接口,迭代器设计模式
		private int i=0;
		public boolean end(){
     //检查序列是否到达末尾
			return i==items.length;
		}
		public Object current(){
     //访问当前对象
			return items[i];
		}
		public void next(){
     //移动下标到虾一位
			if(i<items.length)
				i++;
		}
	}
	public Selector selector(){
     //返回一个内部类
		return new SequenceSelector();
	}
	public static void main(String[] args){
     
		Sequence sequence=new Sequence(10);
		for(int i=0;i<10;i++)
			sequence.add(Integer.toString(i));//初始化外部类对象中的数组
		Selector selector=sequence.selector();//创建内部类对象,指向接口,接口调用实例的具体方法
		while(!selector.end()){
     
			System.out.print(selector.current()+" ");//输出外部类数组中元素
			selector.next();
		}
	}
}
/*Output:
1 2 3 4 5 6 7 8 9
*/

从内部类可以看到,内部类中使用了外部类的private类型的数组item,由此可以看出,内部类对象拥有创建它外部类对象的所有成员的访问权

原因:外部类创建内部类对象时,内部类对象会捕获一个指向外部类对象的引用,通过这个引用可以访问外部类的所有成员。

10.5使用.this和.new

生成外部类对象引用的方法:外部类的名字后面紧跟圆点.和this,在编译期就会接受检查,没有运行开销

如果想让某个对象创建内部类对象,可以使用**.new**的语法来创建。

public class Parcel3 {
     
	class Contents{
     
		private int i=11;
		public int value(){
     
			return i;
		}
	}
	class Destination{
     
		private String label;
		public Destination(String whereTo) {
     
			label=whereTo;
		}
		String readLabel(){
     
			return label;
		}
	}
	public static void main(String[] args){
     
		Parcel3 p=new Parcel3();
		Parcel3.Contents c=p.new Contents();//使用了.new语法创建内部类对象
		Parcel3.Destination d=p.new Destination("Tasmania"); 
	}
}

10.6 内部类向上转型

内部类转型为基类或者为接口时,就获得了该基类或者接口的引用,此时,接口的实现完全不可见,并且不可用,内部类隐藏了实现的细节

interface Destination{
     
	String readLabel();
}
interface Contents{
     
	int value();
}
public class Parcel4 {
     
	private class PContents implements Contents{
     
		private int i=11;
		public int value(){
     
			return i;
		}
	}
	protected class PDestination implements Destination{
     
		private String label;
		public PDestination(String whereTo) {
     
			label=whereTo;
		}
		public String readLabel(){
     
			return label;
		}
	}
	public Destination destination(String s){
     //通过外部类方法创建内部类对象,并向上转型
		return new PDestination(s);
	}
	public Contents contents(){
     
		return new PContents();
	}
	public static void main(String[] args){
     
		Parcel4 p=new Parcel4();
		Destination c=p.destination("Tasmania");//向上转型为Destination接口
		Contents d=p.contents();
		//不能使用下面这种方法,因为PContents内部类为私有的
		//Parcel4.PContents pc=p.new PContents(); 
	}
}

我们可以看到外部类Parcel4中:

内部类PContents是private类型的,除了Parcel4外,别人无法访问它。

内部类PDestination是protected,也仅仅只有Parcel4及其子类和同一个包中的类才具有访问权限。

所以客户端程序员访问这两个内部类是受到限制的,但是可以通过访问Parcel4中的destination()方法和contents()方法获取接口的引用,然而内部类具体的实现被隐藏,客户端程序员并不知道,也不需要知道,只需要调用接口中相应的方法就好了。

10.7 内部类的分类

在Java中内部类主要分为

10.7.1 常用的四种内部类介绍

  • 成员内部类

    • 类比成员方法

    • 成员内部类内部不允许存在任何static变量或方法 正如成员方法中不能有任何静态属性 (成员方法与对象相关、静态属性与类有关)

    • class Outer {
               
          private String name = "test";
          public  static int age =20;
      
          class Inner{
               
              public static int num =10;
              public void fun()
              {
               
                  System.out.println(name);
                  System.out.println(age);
              }
          }
      }
      public class Test{
               
          public static void main(String [] args)
          {
               }
      }
      
    • Java编程思想(第4版本)1-15章笔记_第5张图片

  • 静态内部类

    • 关键字static可以修饰成员变量、方法、代码块、其实还可以修饰内部类,使用static修饰的内部类我们称之为静态内部类,静态内部类和非静态内部类之间存在一个最大的区别,非静态内部类在编译完成之后会隐含的保存着一个引用,该引用是指向创建它的外围类,但是静态类没有。没有这个引用就意味着:
      1.静态内部类的创建不需要依赖外部类可以直接创建。
        2.静态内部类不可以使用任何外部类的非static类包括属性和方法)但可以存在自己的成员变量。

    • class Outer {
               
          public String name = "test";//Inner类无法调用外部任何非static的类、属性或者方法
          private static int age =20;
      
          static class Inner{
               
              private String name = "inner_test";
              public void fun()
              {
               
                  System.out.println(name);
                  System.out.println(age);
              }
          }
      }
      public class Test{
               
          public static void main(String [] args)
          {
               
              Outer.Inner in = new Outer.Inner();
              in.fun();
          }
      }
      /*	output:
          inner_test
          20
      */
      
  • 方法内部类

    • 方法内部类顾名思义就是定义在方法里的类

    • 方法内部类不允许使用访问权限修饰符(public、private、protected)均不允许。

      • class Outer{
                   
            private int num =5;
            public void dispaly(final int temp)
            {
                   
                //方法内部类即嵌套在方法里面
                public class Inner{
                   //不可用权限修饰符修饰方法内部类
                }
            }
        }
        public class Test{
                   
            public static void main(String[] args)
            {
                   }
        }
        
      • Java编程思想(第4版本)1-15章笔记_第6张图片

    • 方法内部类对外部完全隐藏,除了创建这个类的方法可以访问它以外,其他地方均不能访问 (换句话说其他方法或者类都不知道有这个类的存在)方法内部类对外部完全隐藏,出了创建这个类的方法可以访问它,其他地方均不能访问。

    • 方法内部类如果想要使用方法形参,该形参必须使用final声明(JDK8形参变为隐式final声明)

      • class Outer{
                   
            private int num =5;
            //普通方法
            public void dispaly(final int temp)
            {
                   
                //方法内部类即嵌套在方法里面
                class Inner{
                   
                    public void fun()
                    {
                   
                        System.out.println(num);
                        temp++;//编译出现异常:final修饰的参数初始化后不能改变
                        System.out.println(temp);
                    }
                }
                //方法内部类在方法里面创建
                new Inner().fun();
            }
        }
        public class Test{
                   
            public static void main(String[] args)
            {
                   
                Outer out = new Outer();
                out.dispaly(2);
            }
        }
        
  • 匿名内部类

    • 匿名内部类就是一个没有名字的方法内部类,因此特点和方法与方法内部类完全一致,除此之外,还有自己的特点:

      • 匿名内部类必须继承一个抽象类或者实现一个接口
      • 匿名内部类没有类名,因此没有构造方法
      //匿名内部类
      //声明一个接口
      interface MyInterface {
               
          //接口中方法没有方法体
          void test();
      }
      class Outer{
               
          private int num = 5;
          public void dispaly(int temp)
          {
               
              //匿名内部类,匿名的实现了MyInterface接口
              //隐藏的class声明
              new MyInterface()
              {
               
                  public void test()
                  {
               
                      System.out.println("匿名实现MyInterface接口");
                      System.out.println(temp);
                  }
              }.test();
          }
      }
      public class Test{
               
          public static void main(String[] args)
          {
               
              Outer out = new Outer();
              out.dispaly(3);
          }
      }
      /*	output:
          匿名实现MyInterface接口
          3
      */
      

10.7.2 方法和作用域内的内部类

可以在一个方法或者在任意的作用域内定义内部类。

方法的作用域内

public class Parcel5{
     
	public Destination destination(String s){
     
		class PDestination implements Destination{
     //内部类在方法中
			private String label;
			private PDestination(String whereTo){
     
				label=whereTo;
			}
			public String readLabel(){
     
				return label;
			}
		}
		return new PDestination(s);//只有在方法作用域内才可以使用作用域内的内部类
	}
	public static void main(String[] args){
     
		Parcel5 p=new Parcel5();
		Destination d=p.destination("Tasmania");
	}
}

在任意作用域内嵌入一个内部类:

public class Parcel6 {
     
  private void internalTracking(boolean b) {
     
    if(b) {
     
      class TrackingSlip {
     
        private String id;
        TrackingSlip(String s) {
     
          id = s;
        }
        String getSlip() {
      return id; }
      }
      TrackingSlip ts = new TrackingSlip("slip");
      String s = ts.getSlip();
    }
    // Can't use it here! Out of scope:
    //! TrackingSlip ts = new TrackingSlip("x");
  }	
  public void track() {
      internalTracking(true); }
  public static void main(String[] args) {
     
    Parcel6 p = new Parcel6();
    p.track();
  }
} ///:~

作用域内的类与其他类共同编译,但是只在作用域内可用,在其他作用域中使用相同的类名不会有命名冲突

10.7.3 匿名内部类

匿名内部类在创建某个对象进行返回时,对该对象的类进行定义。类的定义和使用放到了一起。下面根据具体例子说明情况。

//197页
interface Contents{
     
	 int value();
}
 
public class Parcel7 {
     
	public Contents contents(){
     
		return new Contents() {
     //匿名内部类,类的使用与定义结合到了一起
			private int i=1;
			public int value(){
     
				return i;
			}
		};
	}
	public static void main(String[] args){
     
		Parcel7 p=new Parcel7();
		Contents c=p.contents();
	}
}

程序中我们可以看到,在contents()方法的内部,在返回了一个Contents()引用的时候,插入了一个类的定义。这里实际的情况是,创建了一个继承自Contents的匿名类的对象,通过new表达式返回的时候,实际上已经向上转型为对Contents的引用了
具体来说,就是下面的代码:

interface Contents{
     
	int value();
}
 
public class Parcel7b {
     
	class MyContents implements Contents{
     //MyContents实现了Contents
		private int i=11;
		public int value(){
     
			return i;
		}
	}
	public Contents contents(){
     
		return new MyContents(); //contents()方法返回了一个MyContents对象,并且向上转型为Contents
	}
	public static void main(String[] args){
     
		Parcel7b p=new Parcel7b();
		Contents c=p.contents();
	}
}

上述匿名内部类使用了默认的构造器生成Contents,也可以使用有参数的构造器。

//197页
 
class Wrapping{
     
	private int i;
	public Wrapping(int x){
     
		i=x;
	}
	public int value(){
     
		return i;
	}
}
 
public class Parcel8 {
     
	public Wrapping wrapping(int x){
     
		return new Wrapping(x){
     //传递了适合基类构造器的参数
			public int value(){
     
				return super.value()*47;
			}
		};
	}
	public static void main(String[] args){
     
		Parcel8 p=new Parcel8();
		Wrapping w=p.wrapping(10);
		System.out.println(w.value());//Wrapping引用匹配到子类的方法
	}
}/*Output:
470*/

匿名内部类中可以看到,传入了一个适合基类构造器的参数。而且尽管Wrapping是一个具有具体实现的类,但是被导出类当作“接口”使用。

匿名内部类没有类名,没办法创建构造函数,那么如何进行初始化工作呢?

//199页
 
public class Parcel10 {
     
	public Destination destination(String dest,float price){
     
		return new Destination() {
     
			private int cost;
			{
     //带有实例初始化
				cost=Math.round(price);
				if(cost>100)
					System.out.println("Over budget!");
			}
			//下面这句话不能通过编译!
			//dest="newTasmania";
			private String label=dest;
			public String readLabel(){
     
				return label;
			}
		};
	}
	public static void main(String[] args){
     
		Parcel10 p=new Parcel10();
		Destination d=p.destination("Tasmania", 101.395F);
	}
}/*Output:
Over budget!*/

在实例初始化的内部,实现了构造器的行为——初始化,但是,你不能重载实例初始化方法, 所以你仅仅有一个这样的构造器。

注意:代码中,你看到了有一行不能通过编译,这是因为,在内部类使用的非final对象将会接受检查——它们不允许被修改。

《Java编程思想》这块,作者写的是内部类使用外部类对象必须要求是final类型的(作者使用的是JAVA8之前的版本),然而我使用的是JAVA8,JAVA8中,匿名内部类使用外部变量不再被强制要求用final修饰,但是要求初始化后的值不能被修改,这是为何呢?

对于final类型来说:编译器编译后,final类型是常量,被存储到了常量池中,在匿名内部类中使用该变量的地方都被替换成了具体的常量值,关于外部类的变量的信息,内部类是不知道的。

对于非final类型来说:传入内部类的仅仅只是传值操作,所以在匿名内部类中改变这个值是无效的。如果在外部类中修改这个值,那么匿名内部类得到的参数值可能已经不是期望中的那个值。所以,在内部类使用外部类的变量时,不允许做任何修改才会避免所以问题。

JAVA8版本对于非final类型会进行检查,要求不允许修改。final变量自然不会被修改,也不会检查,JAVA8以前的版本要求必须是final变量才能给匿名内部类使用

10.7.4 再访工厂方法

//201页
 
interface Game{
     
	boolean move();
}
interface GameFactory{
     
	Game getGame();
}
 
class Checkers implements Game{
     
	private Checkers(){
     }//构造器为private,不能直接创建对象
	private int moves=0;
	private static final int MOVES=3;
	public boolean move(){
     
		System.out.println("Checkers move "+moves);
		return ++moves!=MOVES;
	}
	public static GameFactory factory=new GameFactory() {
     //单一的工厂对象
		public Game getGame(){
     
			return new Checkers();
		}
	};
}
 
class Chess implements Game{
     
	private Chess(){
     }//构造器为private,不能直接创建对象
	private int moves=0;
	private static final int MOVES=4;
	public boolean move(){
     
		System.out.println("Chess move "+moves);
		return ++moves!=MOVES;
	}
	public static GameFactory factory=new GameFactory() {
     //单一的工厂对象
		public Game getGame(){
     
			return new Chess();
		}
	};
}
 
public class Games {
     
	public static void playGame(GameFactory factory){
     //不同的工厂对象生成不同的具体类的对象
		//GameFactory接口调用相应的getGame()方法返回不同的Game对象后向上转型
        Game s=factory.getGame();
		while(s.move());//Game接口自动找到相应实现类的move()方法
	}
	public static void main(String[] args){
     
		playGame(Checkers.factory);//传入Checkers类中的中工厂对象
		playGame(Chess.factory);//闯入Chess类中的工厂对象
	}
}
/*Output:
Checkers move 0
Checkers move 1
Checkers move 2
Chess move 0
Chess move 1
Chess move 2
Chess move 3
*/

可以看到,Chess和Checker类中的构造器均为private类型的,所以不能直接创建该类的对象。但是,我们可以通过这两个类中的静态(单例)工厂对象来创建属于本类的对象。

10.7.5 嵌套类(静态内部类)

如果不想使内部类对象与外部类对象相互联系,那么可以将内部类声明为static,这就是嵌套类。
我们知道,非static内部类必须通过外部类对象创建并且获得一个该外部类对象的引用。然而,对于static类型的内部类:
1、创建静态内部类对象,不需要外部类的对象
2、静态内部类中不能使用外部类的非静态成员(因为没有外部类对象的引用)
3、静态内部类中可以创建staic类中的数据和字段(普通内部类不可以,因为它通过外部类对象创建,不属于类成员)

在main中没有使用外部类对象就创建了内部类对象,因为创建的对象的方法是static的,而且内部类也是static的。

//202页
 
public class Parcel11 {
     
    interface Contents{
     
		 int value();
	}
	public static class ParcelContents implements Contents{
     //静态内部类
		private static int i=11;//静态内部类中的静态变量
		public int value(){
     
			return i;
		}
	}
	protected static class ParcelDestination implements Destination {
     
	    private String label;
	    private ParcelDestination(String whereTo) {
     
	      label = whereTo;
	    }
	    public String readLabel() {
      return label; }	
	    // Nested classes can contain other static elements:
	    public static void f() {
     }
	    static int x = 10;
	    static class AnotherLevel {
     
	      public static void f() {
     }
	      static int x = 10;
	    }
	  }
	  public static Destination destination(String s) {
     
	    return new ParcelDestination(s);
	  }
	  public static Contents contents() {
     
	    return new ParcelContents();
	  }
	public static void main(String[] args){
     
		Contents c = contents();//创建静态内部类对象,不需要外部类的对象
        Destination d = destination("Tasmania");
	}
}

10.7.6 接口内部的类

//202页
 
public interface  ClassInInterface {
     
	void howdy();
	static class Test implements ClassInInterface{
     
		public void howdy(){
     
			System.out.println("Howdy");
		}
	}
	public static void main(String[] args){
     
		new Test().howdy();
	}
}
/*	Output:
	Howdy
*/

正常情况下,接口内部内不能放置任何代码,但是嵌套类可以作为接口的一部分。

接口中类自动地是public和static的。类是static了,仅仅只是将该类置于接口的命名空间里,不违反接口的规则。同时,该嵌套类可以实现它命名空间下的接口。

10.7.7 局部内部类(成员内部类)

可以在类中创建匿名内部类,静态内部类,普通内部类,也可以在代码块里创建内部类,典型的方式就是在一个方法体的内部里面创建。局部内部类不能有访问修飾符,因为它不是外部类的一部分,但是它可以方位当前代码块内的常量(Java8中传入到内部类的值不一定是final类型的,只要初始化后不被修改即可),以及此外围类的所有成员。
创建局部内部类和创建匿名内部类比较:

//214页
 
interface Counter{
     
	int next();//用于返回序列中的下一个值
}
 
public class LocalInnerClass {
     
	private int count=0;
	Counter getCounter(final String name){
     
		class LocalCounter implements Counter{
     //局部内部类,定义在方法中
			public LocalCounter(){
     
				System.out.println("LocalCounter()");
			}
			public int next(){
     
				System.out.print(name);
				return count++;
			}
		}
		return new LocalCounter();
	}
	Counter getCounter2(final String name){
     
		return new Counter(){
     //匿名内部类
			{
     
				System.out.println("Counter()");
			}
			public int next(){
     
				System.out.print(name);
				return count++;
			}
		};
	}
	public static void main(String[] args){
     
		LocalInnerClass lic=new LocalInnerClass();//创建外部类对象
		Counter c1=lic.getCounter("Local inner");//通过外部类对象调用方法创建局部内部类
		Counter c2=lic.getCounter2("Anonymous inner");//通过外部类对象创建匿名内部类
		for(int i=0;i<5;i++)
			System.out.println(c1.next());
		for(int i=0;i<5;i++)
			System.out.println(c2.next());
	}
}
/*Output:
LocalCounter()
Counter()
Local inner0
Local inner1
Local inner2
Local inner3
Local inner4
Anonymous inner5
Anonymous inner6
Anonymous inner7
Anonymous inner8
Anonymous inner9*/

分别创建了局部内部类LocalCounter类和匿名内部类实现了Counter接口,该接口用于返回序列中的下一个值,这两个内部类具有了相同行为和能力。

局部内部类和匿名内部类的区别:局部内部类的名字在方法外是不可见的,局部内部类可以重载构造器,而匿名内部类只能用于实例初始化,没有构造器。

所以当我们需要不止一个内部类的对象时,应该选择局部内部类而不是匿名内部类。

10.8 为什么需要内部类?

内部类使得多重继承的解决方案变得完整,一个外部类可以实现多个接口。然而,只有继承一个抽象的类或具体的类,此时如果想继承多个抽象的类或具体的类,那么内部类可以解决多重继承中的问题。

//205页
 
class D{
     }
abstract class E{
     }
class Z extends D{
     //外部了继承了D类
	E makeE(){
     //
		return new E(){
     };//匿名内部类继承了E类,与外部类实现了多重继承
	}
}
 
public class MultiImplementation {
     
	static void takesD(D d){
     }//接受D类及其子类的引用
	static void takesE(E e){
     }//接受E类及其子类的引用
	public static void main(String[] args){
     
		Z z=new Z();
		takesD(z);
		takesE(z.makeE());
	}
}

Z类实现了多重继承,外部类继承了D类,匿名内部类继承了E类。

1.8.1闭包与回调

闭包:是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。内部类是面向对象的闭包,它不仅包含了外部类对象的信息,还自动拥有一个指向该外部类对象的引用,在此作用域下,内部类有权操作所有的成员,包括private成员。

Java语言中没有包括指针,通过内部类提供的闭包功能实现了回调功能。

//206页
 
interface Incrementable{
     //Incrementable中含有一个increment()方法
	void increment();
}
 
class Callee1 implements Incrementable{
     //Callee1类实现了Incrementable接口
	private int i=0;
	public void increment(){
     
		i++;
		System.out.println(i);
	}
}
 
class MyIncrement{
     //MyIncrement类中创建了自己的increment()方法
	public void increment(){
     
		System.out.println("Other operation");
	}
	static void f(MyIncrement mi){
     
		mi.increment();
	}
}
 
class Callee2 extends MyIncrement{
     //Callee2类继承了MyIncrement类,拥有了不同的increment()方法
	private int i=0;
	public void increment(){
     
		super.increment();
		i++;
		System.out.println(i);
	}
	private class Closure implements Incrementable{
     //内部类实现了Incrementable接口,拥实现了该接口中的increment()方法
		public void increment(){
     
			Callee2.this.increment();
		}
	}
	Incrementable getCallbackReference(){
     //产生内部类Closure的对象
		return new Closure();
	}
}
 
class Caller{
     
	private Incrementable callbackReference;//存入Incrementable引用
	Caller(Incrementable cbh){
     //构造方法中,要求接受Incrementable类型的引用
		callbackReference=cbh;
	}
	void go(){
     
		callbackReference.increment();//Incrementable引用自动调用相应的increment()方法
	}
}
 
public class Callbacks {
     
	public static void main(String[] args){
     
		Callee1 c1=new Callee1();
		Callee2 c2=new Callee2();
		MyIncrement.f(c2);//调用自己MyIncrement中的方法
		Caller caller1=new Caller(c1);//Caller构造函数要求Incrementable类型的接口
		Caller caller2=new Caller(c2.getCallbackReference());
		caller1.go();
		caller1.go();
		caller2.go();
		caller2.go();
	}
}
/*Output:
Other operation
1
1
2
Other operation
2
Other operation
3
*/

从上述案例中可以看到Callee1作为外部类实现接口和Callee2中Closure作为内部类实现接口的区别。Callee2类继承了MyIncrement类,该类中的Increment()方法与接口中的不同,此时想用接口中的该方法,就应该用内部类实现接口,而不是用外部类实现接口,因为那样会覆盖继承自MyIncrement类中的方法。

回调:在Callee2类中,通过外部类对象调用getCallbackReferemce()方法获得了内部类Closure类的对象,这个内部类对象提供了一个返回Callee2的钩子。在main中创建了Caller类的对象caller2,并且存入了一个Incrementable类型的引用,该引用指向了Callee2的对象c2创建的内部类对象Closure,并且通过调用caller2.go()方法,调用了该内部类对象Closure中的increment()方法。
这说明无论谁获得IncrementTable的引用,都只能调用increment()方法。

1.8.2内部类与控制框架

应用程序框架就是被设计用以解决某类特定问题的一个类或一组类。要运用某个应用程序框架,通常是继承一个或多个类,并覆盖某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的通用解决方案。
控制框架是一类特殊的应用程序框架,它用来相应事件的需求。
下面介绍一个具体的案例分析:一个控制框架实现温室的操作:控制灯光、水、温度调节器的开关,以及响铃的重新启动系统,每个行为都是完全不同的。
首先创建一个抽象的事件类

//208页
 
public abstract class Event{
     
	private long eventTime;
	protected final long delayTime;//延迟时间
	public Event(long delayTime){
     
		this.delayTime=delayTime;
		start();//创建Event对象时,调用start()方法
	}
	public void start(){
     //获取当前时间,事件每次重新启动以后,都能运行该方法,重新获取当前时间
		eventTime=System.nanoTime()+delayTime;
	}
	public boolean ready(){
     //判断何时可以运行action()方法,此处是基于时间判断
		return System.nanoTime()>=eventTime;
	}
	public abstract void action();//action()方法在子类中具体时间,不同的事件有不同的操作
}

Event类是一个抽象类,该类的构造函数初始化了事件的延迟时间以及当前时间。其中start()方法可以在时间重新启动时再次调用,再次确定当前时间,

**ready()**方法基于时间来控制动作是否发生,**action()**方法的具体操作依据事件的不同,应该在不同的子类有不同的实现。

其次,创建一个管理并触发事件的实际控制框架。

//209页
import java.util.*;
 
public class Controller{
     
	private List<Event> eventList=new ArrayList<Event>();//List用于存放各个Event事件类
	public void addEvent(Event c){
     //向List中添加事件
		eventList.add(c);
	}
	public void run(){
     //时间执行
		while(eventList.size()>0){
     //首先判断List容器中是否还有容器
			for(Event e:new ArrayList<Event>(eventList))//依次遍历容器中的所有事件
				if(e.ready()){
     //基于时间判断事件是否执行
					System.out.println(e);//输出对应的事件(每个事件中都有toString()方法显示该事件信息)
					e.action();//执行对应的事件
					eventList.remove(e);//从List列表中移除该事件
				}
		}
	}
}

控制框架中的容器List存储了不同事件,run方法通过遍历这些事件,使得这些事件输出信息,并执行相应的action()操作,最后再从List容器中移除。

通过以上两个类,我们创建了事件和控制事件操作的控制框架,但是我们并不清除Event到底做什么,也就是说并不知道事件具体的操作是在什么。控制框架是不变的,它存入不同的事件,对它们进行操作、控制。然而,事件是变的,我们有各种事件,每种事件也会有不同的操作。

那么怎么简便实现这样的操作呢?前边提过内部类实现多重继承的方法:
我们通过创建一个外部类继承控制框架,使得控制框架只有一个不会发生改变。不同的事件创建不同内部类使其具有不同的属性、操作、方法。而且该内部类可以轻而易举的使用外部类的对象,这使得我们的程序更加简便易行。

//210页
 
public class GreenhouseControls extends Controller{
     
	private boolean light=false;
	public class LightOn extends Event{
     //开灯,内部类继承了Event事件,创建时调用Event事件初始化
		public LightOn(long dealyTime){
     
	    	super(dealyTime);
	    }
		public void action(){
     //执行开灯操作
			light=true;
		}
		public String toString(){
     //输出开灯信息
			return "Light is on";
		}
	}
	public class LightOff extends Event{
     //关灯
		public LightOff(long dealyTime){
     
	    	super(dealyTime);
	    }
		public void action(){
     //执行关灯操作
			light=false;
		}
		public String toString(){
     //输出关灯信息
			return "Light is Off";
		}
	}
	private boolean water=false;
	public class WaterOn extends Event{
     //开水龙头
		public WaterOn(long dealyTime){
     
	    	super(dealyTime);
	    }
		public void action(){
     //执行开水头操作
			water=true;
		}
		public String toString(){
     
			return "Greenhouse water is on";
		}
	}
	public class WaterOff extends Event{
     //关水龙头
		public WaterOff(long dealyTime){
     
	    	super(dealyTime);
	    }
		public void action(){
     //执行关水龙头操作
			water=false;
		}
		public String toString(){
     
			return "Greenhouse water is off";
		}
	}
	private String thermostat="Day";
	public class ThermostatNight extends Event{
     //夜晚
		public ThermostatNight(long delayTime){
     
			super(delayTime);
		}
		public void action(){
     //执行夜晚操作
			thermostat="Night";
		}
		public String toString(){
     
			return "ThermostatDay on night setting";
		}
	}
	public class ThermostatDay extends Event{
     //白天
		public ThermostatDay(long delayTime){
     
			super(delayTime);
		}
		public void action(){
     //执行白天操作
			thermostat="Day";
		}
		public String toString(){
     
			return "ThermostatDay on day setting";
		}
	}
	public class Bell extends Event{
     //响铃
		public Bell(long delayTime){
     
			super(delayTime);
		}
		public void action(){
     
			addEvent(new Bell(delayTime));
		}
		public String toString(){
     //执行响铃操作
			return "Bing!";
		}
	}
	public class Restart extends Event{
     //重新启动系统
		private Event[] eventList;		
		public Restart(long delayTime,Event[] eventList){
     
			super(delayTime);
			this.eventList=eventList;
			for(Event e:eventList)
				addEvent(e);
		}
		public void action(){
     //执行重新启动系统操作:将本对象Event数组中对象重新添加到控制框架中
			for(Event e:eventList){
     
				e.start();//每个事件重新启动,重新获取当前时间
				addEvent(e);//每个事件添加到控制事件中
			}
			this.start();//重新启动系统事件也要重新获取当前时间
			addEvent(this);//重新启动系统事件添加至控制事件中
		}
		public String toString(){
     
			return "Restarting system";
		}
	}
	public static class Terminate extends Event{
     //关闭系统
		public Terminate(long delayTime){
     
			super(delayTime);
		}
		public void action(){
     //关闭系统操作
			System.exit(0);
		}
		public String toString(){
     
			return "Terminating";
		}
	}
}

可以看出,GreenhouseControls继承了一个控制框架,并且该类中有不同的内部类,它们代表了不同的事件。其中light、water和thermostat是关于灯、水、白天黑夜的事件,它们是基本的事件,通过添加相应的操作完成事件的发生。

Bell和Restart比较特殊

Bell控制响铃,在它的action()中,它每次都会重新添加一个响铃事件,所以过一会儿就会发生一次响铃事件。

Restart类中存入了一个EventList数组,它其中包含了设置的不同的Event事件,一旦执行Restart重新启动系统事件,即调用了该类的action()方法,那么这个EventList数组中的所有事件将被重新添加到控制框架中,重新被执行相应的操作(当然,添加事件之前,会从新更新它们各自事件的当前时间),最后,重新启动的这个事件也要被添加到控制框架中,不要忘记了,它自身也是一个事件

下面,我们创建一个类用来添加各种不同的事件,并执行各种不同的操作。

//211页
 
 
public class GreenhouseControler{
     
	public static void main(String[] args){
     
		GreenhouseControls gc=new GreenhouseControls();//创建控制温室的对象
		gc.addEvent(gc.new Bell(900));//首先向框架中添加响铃对象
		Event[] eventList={
     //一个包含了各种事件的Event数组
				gc.new ThermostatNight(0),//外部类创建了不同的事件,这些内部类事件具有外部类的访问权限
				gc.new LightOn(200),
				gc.new LightOff(400),
				gc.new WaterOn(600),
				gc.new WaterOff(800),
				gc.new ThermostatDay(1400),
		};
		//第一步,重新启动系统的构造函数向框架中添加evenList中所有的事件
		//第二步,添加重新启动系统这个事件到框架中
		gc.addEvent(gc.new Restart(2000,eventList));
		if(args.length==1)//如果main方法中传入的String大小为1则添加关闭系统事件到末尾
			gc.addEvent(
					new GreenhouseControls.Terminate(
						new Integer(args[0])));
		gc.run();//gc对象执行控制框架的run方法,开始执行相应的操作
	}
}/*Bing!
ThermostatDay on night setting
Light is on
Light is Off
Greenhouse water is on
Greenhouse water is off
ThermostatDay on day setting
Restarting system
Terminating
*/

注意:main中添加重新启动系统事件时的顺序。

首先Restart类进行初始化,此时会初始化重新启动事件对象,

然后将eventList数组中的所有事件添加到控制框架中,此时初始化完毕。

然后回到main中将重新启动系统事件添加到控制框架中,这就使我们明白了输出的顺序。重新启动系统执行action()操作时,会将eventList中所有的事件重新获取当前时间并添加到控制框架中,最后再将自己也添加到控制框架中,不要忘记,重新启动系统也是一个Event事件

备注:使用上述几个类,注意包访问权,main中设置参数以免循环调用!

10.9 内部类与外部类的关系

  • 对于非静态的内部类,内部类的创建依赖外部类的实例对象,在没有外部类实例之前是无法创建内部类的。
  • 内部类可以直接访问外部类的元素(包括私有域)—外部类在内部类之前创建,创建内部类时会将外部类的对象传入
class Outer{
     
    //成员变量  与对象有关
    private String msg;
    private int age;
    //--------------------------
    class Inner{
     
        public void dispaly()
        {
     
            //此处有一个隐藏的Outer.this
            //Outer.this.msg="test";
            msg = "test";
            age = 20;
            System.out.println(msg);
            System.out.println(age);
        }
    }
    //--------------------------
    public void test()
    {
     
        Inner in = new Inner();
        in.dispaly();
    }
}
public class Test{
     
    public static void main(String[] args)
    {
     
        Outer out = new Outer();
        out.test();
    }
}
  • 外部类可以通过内部类的引用间接访问内部类元素 – -要想访问内部类属性,必须先创建内部类对象
class Outer{
     
    public void dispaly()
    {
     
        //外部类通过创建内部类的对象间接访问内部类元素
        Inner in = new Inner();
        in.dispaly();
    }
    class Inner{
     
        public void dispaly()
        {
     
            System.out.println("内部类");
        }
    }
}
public class Test1{
     
    public static void main(String[] args)
    {
     
        Outer out = new Outer();
        out.dispaly();
    }
}
  • 内部类是一个相对独立的个体,与外部类没有关系。

10.10 内部类的继承、覆盖、标识符

10.10.1 继承

如何继承一个内部类?根据前面的内容,内部类中会有一个捕获自创建它外部类对象的引用,如果继承该内部类,这个外部类的引用也要被连接初始化,内部类中也不存在可连接的默认对象。

//212页
 
class WithInner{
     
	class Inner{
     }
}
 
public class InheritInner extends WithInner.Inner{
     
	//public InheritInner(){} 不能使用该构造器,因为没有获取父类外部类的引用
	public InheritInner(WithInner wi) {
     
		wi.super();//通过父类外部类的引用初始化继承的内部类
	}
	public static void main(String[] args){
     
		WithInner wi=new WithInner();
		InheritInner i1=new InheritInner(wi);
	}
}

在继承内部类的类中,必须获取一个创建内部类的外部类对象的引用,通过这个引用调用内部类父类进行初始化操作。
**注意:**这里Inner内部类我们使用的是一个默认构造函数,是没有参数的。如果有参数又该如何呢:

class WithInner{
     
	class Inner{
     
		public Inner(int i) {
     }//带有参数的构造函数
	}
}
 
public class InheritInner extends WithInner.Inner{
     
	//public InheritInner(){} 不能使用该构造器,因为没有获取父类外部类的引用
	public InheritInner(WithInner wi) {
     
		wi.super(1);//调用父类构造器时,需要一个符合父类构造器的参数
	}
	public static void main(String[] args){
     
		WithInner wi=new WithInner();
		InheritInner i1=new InheritInner(wi);
	}
}

此时内部类Inner中有一个带有参数的构造器,此时我们在继承内部类的InheritInner类中,调用父类时应该添加相应的参数

10.10.2 覆盖

如果有一个类,并且该类有一个内部类,那么创建一个新类继承该外部类,然后在新类中重新创建此内部类会覆盖父类中的内部类吗?

//212页
 
class Egg{
     
	private Yolk y;
	protected class Yolk{
     
		public Yolk(){
     
			System.out.println("Egg.Yolk()");
		}
	}
	public Egg(){
     
		System.out.println("New Egg()");
		y=new Yolk();
	}
}
 
public class BigEgg extends Egg{
     //继承了一个外部类
	public class Yolk{
     //重写继承外部类中的内部类
		public Yolk(){
     
			System.out.println("BigEgg.Yolk()");
		}
	}
	public static void main(String[] args){
     
		new BigEgg();
	}
}/*Output:
New Egg()
Egg.Yolk()*/

创建自身对象时,会调用父类构造器,然后父类构造器中顶一起了内部类,然后输出的结果并不是子类中的内部类,也就是说父类并没有获取子类内部类的对象,也就不存在转型。两个内部类没有什么关系,它们各自在自己的命名空间内

当BigEgg中的内部类明确继承了BigEgg父类Egg外部类中的内部类Yolk类。

//213页
 
class Egg2{
     
	private Yolk y=new Yolk();//先与构造器前初始化,调用内部类构造器
	protected class Yolk{
     
		public Yolk(){
     //内部类构造函数
			System.out.println("Egg2.Yolk()");
		}
		public void f() {
     //内部类f()方法
			System.out.println("Egg2.Yolk().f()");
		}
	}
	public Egg2(){
     //初始化之前,先初始化字段
		System.out.println("New Egg2()");
	}
	public void insertYolk(Yolk yy){
     //获取一个Yolk类或子类的引用
		y=yy;
	}
	public void g(){
     
		y.f();//调用y引用指向的类的f()方法
	}
}
 
public class BigEgg2 extends Egg2{
     
	public class Yolk extends Egg2.Yolk{
     
		public Yolk(){
     
			System.out.println("BigEgg2.Yolk()");
		}
		public void f(){
     
			System.out.println("BigEgg2.Yolk().f()");
		}
	}
	public BigEgg2(){
     
		insertYolk(new Yolk());//首先初始化父类BigEgg,然后调用本类中的Yolk类的构造函数,然后调用inserYolk()方法
	}
	public static void main(String[] args){
     
		Egg2 e2=new BigEgg2();//调用BigEgg2()构造器,向上转型为Egg2类
		e2.g();
	}
}/*Output:
Egg2.Yolk()
New Egg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk().f()
*/

BigEgg2.Yolk2明确的继承了Egg2类中的内部类Yolk,并且覆盖了f()方法。**insertYolk()方法,将子类BigEgg2中的内部类对象转型为父类Egg中的内部类,当g()调用f()方法时,覆盖的f()**方法被执行。

可能很多人第一次看不懂这个输出的结果,我分析了程序的执行步骤

  1. main()方法中new BigEgg2()调用了BigEgg2()构造器,由于BigEgg2()继承自Egg2,所以首先初始化父类Egg2类
  2. 父类Egg2初始化时,首先对字段进行初始化,所以初始化了Yolk对象的y,new Yolk()时就当用了内部类Yolk的构造函数
  3. 执行Egg2.Yolk的构造函数,第一次输出产生“Egg2.Yolk()
  4. 执行Egg2的构造函数,第二次输出产生“New Egg2()
  5. 返回到子类BigEgg2中,执行insertYolk(new Yolk(),首先执行方法中的new Yolk(),调用BigEgg2.Yolk的构造函数
  6. 由于BigEgg2.Yolk继承自Egg2.Yolk,所以先调用Egg.Yolk中的构造函数,第三次输出产生”Egg2.Yolk()
  7. 返回到子类BigEgg2.Yolk中,执行构造函数,第四次输出产生”BigEgg2.Yolk()
  8. 此时BigEgg2的构造全部结束,返回到main中向上转型为Egg2
  9. 执行e2.g()方法,调用y.f()方法,由于Egg2中Egg2.Yolk的引用y存入的是子类BigEgg2.Yolk的引用,所以会调用BigEgg2.Yolk中的f()方法
  10. 执行BigEgg2.Yolk中的f()方法,第五次输出产生”BigEgg2.Yolk().f()

10.10.3 标识符

由于每个类都会产生一个.class文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个”meta-class“,叫做Class对象),内部类也会生成一个.class文件以包含它们的Class对象信息。这些类的命名有严格的规则:外围类的名字,加上”$“,再加上内部类的名字。例如,LocalInnerClass.java生成的.class文件包括:

Counter,class
LocalInnerClass$1.class
LocalInnerClass$1LocalCounter.class
LocalInnerClass.class

如果是匿名内部类,编译器会简单的产生一个数字作为其标识符。如果内部类嵌套在别的内部类中,只需将它们的名字加在其外部类标识符与$的后面。

10.11 总结

在内部类章节中,我们了解到了一个普通内部类必须要通过外部类对象来创建,并且它会获得一个外部类对象的引用,这样它就能访问外部类对象中的所有成员了。然而对于一个嵌套类也就是静态内部类来说,它不需要外部类对象就能创建对象,因为它是静态的属于类本身,不过这也限制了它的操作,静态内部类不能访问非静态的外部类对象。

除此之外,普通的内部类也不能包含static的字段和数据,我把它想象成属于外部类对象的某个”方法“,显然,普通内部类只能通过外部类对象创建,static数据和字段不属于对象,所以会同类一起编译存储在静态区,那么这个静态区属于哪个类其实是未知的。

在本章中,我们也发现了,JAVA相对本书版本已经更新了,在JAVA8的版本中,内部类接收的参数已经不必必须声明为final常量了(现在你也可以这么做),但是如果不声明为final,编译器要求这个值初始化后不能被修改,否则会报错,这是因为在Java中的传值操作,如果这个值在外部类中被修改,那么内部类得到的数值可能已经发生改变,得到了不是期望的值,这会带来很大的问题。

作用域中的类与其它类共同编译,但只在作用域内可用,在其他作用于使用相同的域名不会有命名冲突。同时,如果一个类继承了一个外部类,并且创建了相同的内部类时,其实并不会被覆盖,只有当这个继承的类创建了继承自父类中内部类的一个类时,才会出现覆盖的可能。

Java内部类完美的实现了多重继承,虽然我们可以通过实现多个接口来实现多重继承,但是如果拥有的是抽象的类或者具体的类时,那只能用内部类才能实现多重继承

通过学习,我们也发现了内部类强大的功能。我们编写了控制温室的案例,用一个外部类继承控制框架,用多个内部类来继承各种事件,我们将控制与行为进行了分离,不仅利于代码的维护,还为开发提供了极大的方便。

第11章 持有对象(容器)

Java编程思想(第4版本)1-15章笔记_第7张图片

​ Java中的容器主要包括 CollectionMap 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。

11.1 Collection

11.1.1 Set

  • TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
  • HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
  • LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。

11.1.2. List

  • ArrayList:基于动态数组实现,支持随机访问。
  • Vector:和 ArrayList 类似,但它是线程安全的。
  • LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈(Stack)、队列(Queue)和双向队列。

11.1.3. Queue

  • LinkedList:可以用它来实现双向队列。
  • PriorityQueue:基于堆结构实现,可以用它来实现优先队列。

11.2 Map

  • TreeMap:基于红黑树实现。
  • HashMap:基于哈希表实现。
  • HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable,并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
  • LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。

深入剖析Java容器原理 请参考:https://blog.csdn.net/qq_34161458/article/details/105633648 同时Java编程思想第 17 章容器深入研究 会继续介绍

第12章 通过异常处理错误

Java 异常处理

异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。

比如说,你的代码少了一个分号,那么运行出来结果是提示是错误 java.lang.Error;如果你用System.out.println(11/0),那么你是因为你用0做了除数,会抛出 java.lang.ArithmeticException 的异常。

异常发生的原因有很多,通常包含以下几大类:

  • 用户输入了非法数据。
  • 要打开的文件不存在。
  • 网络通信时连接中断,或者JVM内存溢出。

这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。-

要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:

  • **检查性异常:**最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
  • 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
  • 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

Exception 类的层次

所有的异常类是从 java.lang.Exception 类继承的子类。

Exception 类是 Throwable 类的子类。除了Exception类外,Throwable还有一个子类Error 。

Java 程序通常不捕获错误。错误一般发生在严重故障时,它们在Java程序处理的范畴之外。

Error 用来指示运行时环境发生的错误。

例如,JVM 内存溢出。一般地,程序不会从错误中恢复。

异常类有两个主要的子类:IOException 类和 RuntimeException 类。

Java编程思想(第4版本)1-15章笔记_第8张图片

在 Java 内置类中(接下来会说明),有大部分常用检查性和非检查性异常。

Java 内置异常类

Java 语言定义了一些异常类在 java.lang 标准包中。

标准运行时异常类的子类是最常见的异常类。由于 java.lang 包是默认加载到所有的 Java 程序的,所以大部分从运行时异常类继承而来的异常都可以直接使用。

Java 根据各个类库也定义了一些其他的异常,下面的表中列出了 Java 的非检查性异常。

异常 描述
ArithmeticException 当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。
ArrayIndexOutOfBoundsException 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。
ArrayStoreException 试图将错误类型的对象存储到一个对象数组时抛出的异常。
ClassCastException 当试图将对象强制转换为不是实例的子类时,抛出该异常。
IllegalArgumentException 抛出的异常表明向方法传递了一个不合法或不正确的参数。
IllegalMonitorStateException 抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程。
IllegalStateException 在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。
IllegalThreadStateException 线程没有处于请求操作所要求的适当状态时抛出的异常。
IndexOutOfBoundsException 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。
NegativeArraySizeException 如果应用程序试图创建大小为负的数组,则抛出该异常。
NullPointerException 当应用程序试图在需要对象的地方使用 null 时,抛出该异常
NumberFormatException 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
SecurityException 由安全管理器抛出的异常,指示存在安全侵犯。
StringIndexOutOfBoundsException 此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小。
UnsupportedOperationException 当不支持请求的操作时,抛出该异常。

下面的表中列出了 Java 定义在 java.lang 包中的检查性异常类。

异常 描述
ClassNotFoundException 应用程序试图加载类时,找不到相应的类,抛出该异常。
CloneNotSupportedException 当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常。
IllegalAccessException 拒绝访问一个类的时候,抛出该异常。
InstantiationException 当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常。
InterruptedException 一个线程被另一个线程中断,抛出该异常。
NoSuchFieldException 请求的变量不存在
NoSuchMethodException 请求的方法不存在

异常方法

下面的列表是 Throwable 类的主要方法:

序号 方法及说明
1 public String getMessage() 返回关于发生的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了。
2 public Throwable getCause() 返回一个Throwable 对象代表异常原因。
3 public String toString() 使用getMessage()的结果返回类的串级名字。
4 public void printStackTrace() 打印toString()结果和栈层次到System.err,即错误输出流。
5 public StackTraceElement [] getStackTrace() 返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底。
6 public Throwable fillInStackTrace() 用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中。

捕获异常

使用 try 和 catch 关键字可以捕获异常。try/catch 代码块放在异常可能发生的地方。

try/catch代码块中的代码称为保护代码,使用 try/catch 的语法如下:

try
{
     
   // 程序代码
}catch(ExceptionName e1)
{
     
   //Catch 块
}

Catch 语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try 后面的 catch 块就会被检查。

如果发生的异常包含在 catch 块中,异常会被传递到该 catch 块,这和传递一个参数到方法是一样。

实例

下面的例子中声明有两个元素的一个数组,当代码试图访问数组的第三个元素的时候就会抛出一个异常。

ExcepTest.java 文件代码:

// 文件名 : ExcepTest.java
import java.io.*;
public class ExcepTest{
     
 
   public static void main(String args[]){
     
      try{
     
         int a[] = new int[2];
         System.out.println("Access element three :" + a[3]);
      }catch(ArrayIndexOutOfBoundsException e){
     
         System.out.println("Exception thrown  :" + e);
      }
      System.out.println("Out of the block");
   }
}

以上代码编译运行输出结果如下:

Exception thrown  :java.lang.ArrayIndexOutOfBoundsException: 3
Out of the block

多重捕获块

一个 try 代码块后面跟随多个 catch 代码块的情况就叫多重捕获。

多重捕获块的语法如下所示:

try{
     
   // 程序代码
}catch(异常类型1 异常的变量名1){
     
  // 程序代码
}catch(异常类型2 异常的变量名2){
     
  // 程序代码
}catch(异常类型2 异常的变量名2){
     
  // 程序代码
}

上面的代码段包含了 3 个 catch块。

可以在 try 语句后面添加任意数量的 catch 块。

如果保护代码中发生异常,异常被抛给第一个 catch 块。

如果抛出异常的数据类型与 ExceptionType1 匹配,它在这里就会被捕获。

如果不匹配,它会被传递给第二个 catch 块。

如此,直到异常被捕获或者通过所有的 catch 块。

实例

该实例展示了怎么使用多重 try/catch。

try {
     
    file = new FileInputStream(fileName);
    x = (byte) file.read();
} catch(FileNotFoundException f) {
      // Not valid!
    f.printStackTrace();
    return -1;
} catch(IOException i) {
     
    i.printStackTrace();
    return -1;
}

throws/throw 关键字:

如果一个方法没有捕获到一个检查性异常,那么该方法必须使用 throws 关键字来声明。throws 关键字放在方法签名的尾部。

也可以使用 throw 关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。

下面方法的声明抛出一个 RemoteException 异常:

import java.io.*;
public class className
{
     
  public void deposit(double amount) throws RemoteException
  {
     
    // Method implementation
    throw new RemoteException();
  }
  //Remainder of class definition
}

一个方法可以声明抛出多个异常,多个异常之间用逗号隔开。

例如,下面的方法声明抛出 RemoteException 和 InsufficientFundsException:

import java.io.*;
public class className
{
     
   public void withdraw(double amount) throws RemoteException,
                              InsufficientFundsException
   {
     
       // Method implementation
   }
   //Remainder of class definition
}

finally关键字

finally 关键字用来创建在 try 代码块后面执行的代码块。

无论是否发生异常,finally 代码块中的代码总会被执行。

在 finally 代码块中,可以运行清理类型等收尾善后性质的语句。

finally 代码块出现在 catch 代码块最后,语法如下:

try{
     
  // 程序代码
}catch(异常类型1 异常的变量名1){
     
  // 程序代码
}catch(异常类型2 异常的变量名2){
     
  // 程序代码
}finally{
     
  // 程序代码
}

实例

ExcepTest.java 文件代码:

public class ExcepTest{
     
  public static void main(String args[]){
     
    int a[] = new int[2];
    try{
     
       System.out.println("Access element three :" + a[3]);
    }catch(ArrayIndexOutOfBoundsException e){
     
       System.out.println("Exception thrown  :" + e);
    }
    finally{
     
       a[0] = 6;
       System.out.println("First element value: " +a[0]);
       System.out.println("The finally statement is executed");
    }
  }
}

以上实例编译运行结果如下:

Exception thrown  :java.lang.ArrayIndexOutOfBoundsException: 3
First element value: 6
The finally statement is executed

注意下面事项:

  • catch 不能独立于 try 存在。
  • 在 try/catch 后面添加 finally 块并非强制性要求的。
  • try 代码后不能既没 catch 块也没 finally 块。
  • try, catch, finally 块之间不能添加任何代码。

声明自定义异常

在 Java 中你可以自定义异常。编写自己的异常类时需要记住下面的几点。

  • 所有异常都必须是 Throwable 的子类。
  • 如果希望写一个检查性异常类,则需要继承 Exception 类。
  • 如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。

可以像下面这样定义自己的异常类:

class MyException extends Exception{
     
}

只继承Exception 类来创建的异常类是检查性异常类。

下面的 InsufficientFundsException 类是用户定义的异常类,它继承自 Exception。

一个异常类和其它任何类一样,包含有变量和方法。

实例

以下实例是一个银行账户的模拟,通过银行卡的号码完成识别,可以进行存钱和取钱的操作。

InsufficientFundsException.java 文件代码:

// 文件名InsufficientFundsException.java
import java.io.*;
 
//自定义异常类,继承Exception类
public class InsufficientFundsException extends Exception
{
     
  //此处的amount用来储存当出现异常(取出钱多于余额时)所缺乏的钱
  private double amount;
  public InsufficientFundsException(double amount)
  {
     
    this.amount = amount;
  } 
  public double getAmount()
  {
     
    return amount;
  }
}

为了展示如何使用我们自定义的异常类,

在下面的 CheckingAccount 类中包含一个 withdraw() 方法抛出一个 InsufficientFundsException 异常。

CheckingAccount.java 文件代码:

// 文件名称 CheckingAccount.java
import java.io.*;
 
//此类模拟银行账户
public class CheckingAccount
{
     
  //balance为余额,number为卡号
   private double balance;
   private int number;
   public CheckingAccount(int number)
   {
     
      this.number = number;
   }
  //方法:存钱
   public void deposit(double amount)
   {
     
      balance += amount;
   }
  //方法:取钱
   public void withdraw(double amount) throws
                              InsufficientFundsException
   {
     
      if(amount <= balance)
      {
     
         balance -= amount;
      }
      else
      {
     
         double needs = amount - balance;
         throw new InsufficientFundsException(needs);
      }
   }
  //方法:返回余额
   public double getBalance()
   {
     
      return balance;
   }
  //方法:返回卡号
   public int getNumber()
   {
     
      return number;
   }
}

下面的 BankDemo 程序示范了如何调用 CheckingAccount 类的 deposit() 和 withdraw() 方法。

BankDemo.java 文件代码:

//文件名称 BankDemo.java
public class BankDemo
{
     
   public static void main(String [] args)
   {
     
      CheckingAccount c = new CheckingAccount(101);
      System.out.println("Depositing $500...");
      c.deposit(500.00);
      try
      {
     
         System.out.println("\nWithdrawing $100...");
         c.withdraw(100.00);
         System.out.println("\nWithdrawing $600...");
         c.withdraw(600.00);
      }catch(InsufficientFundsException e)
      {
     
         System.out.println("Sorry, but you are short $"
                                  + e.getAmount());
         e.printStackTrace();
      }
    }
}

编译上面三个文件,并运行程序 BankDemo,得到结果如下所示:

Depositing $500...

Withdrawing $100...

Withdrawing $600...
Sorry, but you are short $200.0
InsufficientFundsException
        at CheckingAccount.withdraw(CheckingAccount.java:25)
        at BankDemo.main(BankDemo.java:13)

通用异常

在Java中定义了两种类型的异常和错误。

  • **JVM(Java虚拟机) 异常:**由 JVM 抛出的异常或错误。例如:NullPointerException 类,ArrayIndexOutOfBoundsException 类,ClassCastException 类。
  • **程序级异常:**由程序或者API程序抛出的异常。例如 IllegalArgumentException 类,IllegalStateException 类。

第13章 字符串

13.1 String 类

​ 字符串广泛应用 在 Java 编程中,在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串。

创建字符串

String string = new String("Hello String");  

字符串长度

String str = "abcdefg";
int len = str.length();

连接字符串

//string1.concat(string2);
"我的名字是 ".concat("csp");

"Hello," + " csp" + "!"

创建格式化字符串

我们知道输出格式化数字可以使用 printf() 和 format() 方法。

String 类使用静态方法 format() 返回一个String 对象而不是 PrintStream 对象。

String 类的静态方法 format() 能用来创建可复用的格式化字符串,而不仅仅是用于一次打印输出。

如下所示:

System.out.printf("浮点型变量的值为 " +
                  "%f, 整型变量的值为 " +
                  " %d, 字符串变量的值为 " +
                  "is %s", floatVar, intVar, stringVar);
//或者
String fs;
fs = String.format("浮点型变量的值为 " +
                   "%f, 整型变量的值为 " +
                   " %d, 字符串变量的值为 " +
                   " %s", floatVar, intVar, stringVar);

String 方法

下面是 String 类支持的方法,更多详细,参看 Java String API 文档:

序号 方法描述
1 char charAt(int index) 返回指定索引处的 char 值。
2 int compareTo(Object o) 把这个字符串和另一个对象比较。
3 int compareTo(String anotherString) 按字典顺序比较两个字符串。
4 int compareToIgnoreCase(String str) 按字典顺序比较两个字符串,不考虑大小写。
5 String concat(String str) 将指定字符串连接到此字符串的结尾。
6 boolean contentEquals(StringBuffer sb) 当且仅当字符串与指定的StringBuffer有相同顺序的字符时候返回真。
7 [static String copyValueOf(char] data) 返回指定数组中表示该字符序列的 String。
8 [static String copyValueOf(char] data, int offset, int count) 返回指定数组中表示该字符序列的 String。
9 boolean endsWith(String suffix) 测试此字符串是否以指定的后缀结束。
10 boolean equals(Object anObject) 将此字符串与指定的对象比较。
11 boolean equalsIgnoreCase(String anotherString) 将此 String 与另一个 String 比较,不考虑大小写。
12 [byte] getBytes() 使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。
13 [byte] getBytes(String charsetName) 使用指定的字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。
14 [void getChars(int srcBegin, int srcEnd, char] dst, int dstBegin) 将字符从此字符串复制到目标字符数组。
15 int hashCode() 返回此字符串的哈希码。
16 int indexOf(int ch) 返回指定字符在此字符串中第一次出现处的索引。
17 int indexOf(int ch, int fromIndex) 返回在此字符串中第一次出现指定字符处的索引,从指定的索引开始搜索。
18 int indexOf(String str) 返回指定子字符串在此字符串中第一次出现处的索引。
19 int indexOf(String str, int fromIndex) 返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始。
20 String intern() 返回字符串对象的规范化表示形式。
21 int lastIndexOf(int ch) 返回指定字符在此字符串中最后一次出现处的索引。
22 int lastIndexOf(int ch, int fromIndex) 返回指定字符在此字符串中最后一次出现处的索引,从指定的索引处开始进行反向搜索。
23 int lastIndexOf(String str) 返回指定子字符串在此字符串中最右边出现处的索引。
24 int lastIndexOf(String str, int fromIndex) 返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索。
25 int length() 返回此字符串的长度。
26 boolean matches(String regex) 告知此字符串是否匹配给定的正则表达式。
27 boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len) 测试两个字符串区域是否相等。
28 boolean regionMatches(int toffset, String other, int ooffset, int len) 测试两个字符串区域是否相等。
29 String replace(char oldChar, char newChar) 返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。
30 String replaceAll(String regex, String replacement) 使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
31 String replaceFirst(String regex, String replacement) 使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
32 [String] split(String regex) 根据给定正则表达式的匹配拆分此字符串。
33 [String] split(String regex, int limit) 根据匹配给定的正则表达式来拆分此字符串。
34 boolean startsWith(String prefix) 测试此字符串是否以指定的前缀开始。
35 boolean startsWith(String prefix, int toffset) 测试此字符串从指定索引开始的子字符串是否以指定前缀开始。
36 CharSequence subSequence(int beginIndex, int endIndex) 返回一个新的字符序列,它是此序列的一个子序列。
37 String substring(int beginIndex) 返回一个新的字符串,它是此字符串的一个子字符串。
38 String substring(int beginIndex, int endIndex) 返回一个新字符串,它是此字符串的一个子字符串。
39 [char] toCharArray() 将此字符串转换为一个新的字符数组。
40 String toLowerCase() 使用默认语言环境的规则将此 String 中的所有字符都转换为小写。
41 String toLowerCase(Locale locale) 使用给定 Locale 的规则将此 String 中的所有字符都转换为小写。
42 String toString() 返回此对象本身(它已经是一个字符串!)。
43 String toUpperCase() 使用默认语言环境的规则将此 String 中的所有字符都转换为大写。
44 String toUpperCase(Locale locale) 使用给定 Locale 的规则将此 String 中的所有字符都转换为大写。
45 String trim() 返回字符串的副本,忽略前导空白和尾部空白。
46 static String valueOf(primitive data type x) 返回给定data type类型x参数的字符串表示形式。

13.2 StringBuffer 和 StringBuilder 类

当对字符串进行修改的时候,需要使用 StringBuffer 和 StringBuilder 类。

和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象

StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。

由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类

public class Test{
     
  public static void main(String args[]){
     
    StringBuffer sBuffer = new StringBuffer("百度官网:");
    sBuffer.append("www");
    sBuffer.append(".baidu");
    sBuffer.append(".com");
    System.out.println(sBuffer);  
  }
}

StringBuffer 方法

以下是 StringBuffer 类支持的主要方法:

序号 方法描述
1 public StringBuffer append(String s) 将指定的字符串追加到此字符序列。
2 public StringBuffer reverse() 将此字符序列用其反转形式取代。
3 public delete(int start, int end) 移除此序列的子字符串中的字符。
4 public insert(int offset, int i) 将 int 参数的字符串表示形式插入此序列中。
5 replace(int start, int end, String str) 使用给定 String 中的字符替换此序列的子字符串中的字符。

下面的列表里的方法和 String 类的方法类似:

序号 方法描述
1 int capacity() 返回当前容量。
2 char charAt(int index) 返回此序列中指定索引处的 char 值。
3 void ensureCapacity(int minimumCapacity) 确保容量至少等于指定的最小值。
4 void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) 将字符从此序列复制到目标字符数组 dst
5 int indexOf(String str) 返回第一次出现的指定子字符串在该字符串中的索引。
6 int indexOf(String str, int fromIndex) 从指定的索引处开始,返回第一次出现的指定子字符串在该字符串中的索引。
7 int lastIndexOf(String str) 返回最右边出现的指定子字符串在此字符串中的索引。
8 int lastIndexOf(String str, int fromIndex) 返回 String 对象中子字符串最后出现的位置。
9 int length() 返回长度(字符数)。
10 void setCharAt(int index, char ch) 将给定索引处的字符设置为 ch
11 void setLength(int newLength) 设置字符序列的长度。
12 CharSequence subSequence(int start, int end) 返回一个新的字符序列,该字符序列是此序列的子序列。
13 String substring(int start) 返回一个新的 String,它包含此字符序列当前所包含的字符子序列。
14 String substring(int start, int end) 返回一个新的 String,它包含此序列当前所包含的字符子序列。
15 String toString() 返回此序列中数据的字符串表示形式。

13.3 正则表达式

正则表达式定义了字符串的模式。

正则表达式可以用来搜索、编辑或处理文本。

正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别。

正则表达式实例

一个字符串其实就是一个简单的正则表达式,例如 Hello World 正则表达式匹配 “Hello World” 字符串。

.(点号)也是一个正则表达式,它匹配任何一个字符如:“a” 或 “1”。

下表列出了一些正则表达式的实例及描述:

正则表达式 描述
this is text 匹配字符串 “this is text”
this\s+is\s+text 注意字符串中的 \s+。匹配单词 “this” 后面的 \s+ 可以匹配多个空格,之后匹配 is 字符串,再之后 \s+ 匹配多个空格然后再跟上 text 字符串。可以匹配这个实例:this is text
^\d+(.\d+)? ^ 定义了以什么开始\d+ 匹配一个或多个数字? 设置括号内的选项是可选的. 匹配 "."可以匹配的实例:“5”, “1.5” 和 “2.21”。

Java 正则表达式和 Perl 的是最为相似的。

java.util.regex 包主要包括以下三个类:

  • Pattern 类:

    pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。

  • Matcher 类:

    Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。

  • PatternSyntaxException:

    PatternSyntaxException 是一个非强制异常类,它表示一个正则表达式模式中的语法错误。

以下实例中使用了正则表达式 .*runoob.* 用于查找字符串中是否包了 runoob 子串:

//实例
import java.util.regex.*;
 
class RegexExample1{
     
   public static void main(String args[]){
     
      String content = "I am noob " +
        "from runoob.com.";
 
      String pattern = ".*runoob.*";
 
      boolean isMatch = Pattern.matches(pattern, content);
      System.out.println("字符串中是否包含了 'runoob' 子字符串? " + isMatch);
   }
}

//实例输出结果为:

字符串中是否包含了 'runoob' 子字符串? true

捕获组

捕获组是把多个字符当一个单独单元进行处理的方法,它通过对括号内的字符分组来创建。

例如,正则表达式 (dog) 创建了单一分组,组里包含"d",“o”,和"g"。

捕获组是通过从左至右计算其开括号来编号。例如,在表达式((A)(B(C))),有四个这样的组:

  • ((A)(B©))
  • (A)
  • (B©)
  • ©

可以通过调用 matcher 对象的 groupCount 方法来查看表达式有多少个分组。groupCount 方法返回一个 int 值,表示matcher对象当前有多个捕获组。

还有一个特殊的组(group(0)),它总是代表整个表达式。该组不包括在 groupCount 的返回值中。

下面的例子说明如何从一个给定的字符串中找到数字串:

import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class RegexMatches
{
     
    public static void main( String args[] ){
     
 
      // 按指定模式在字符串查找
      String line = "This order was placed for QT3000! OK?";
      String pattern = "(\\D*)(\\d+)(.*)";
 
      // 创建 Pattern 对象
      Pattern r = Pattern.compile(pattern);
 
      // 现在创建 matcher 对象
      Matcher m = r.matcher(line);
      if (m.find( )) {
     
         System.out.println("Found value: " + m.group(0) );
         System.out.println("Found value: " + m.group(1) );
         System.out.println("Found value: " + m.group(2) );
         System.out.println("Found value: " + m.group(3) ); 
      } else {
     
         System.out.println("NO MATCH");
      }
   }
}

以上实例编译运行结果如下:

# Found value: This order was placed for QT3000! OK?
# Found value: This order was placed for QT
# Found value: 3000
# Found value: ! OK?

正则表达式语法

在其他语言中,\ 表示:我想要在正则表达式中插入一个普通的(字面上的)反斜杠,请不要给它任何特殊的意义。

在 Java 中,\ 表示:我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。

所以,在其他的语言中(如Perl),一个反斜杠 ** 就足以具有转义的作用,而在 Java 中正则表达式中则需要有两个反斜杠才能被解析为其他语言中的转义作用。也可以简单的理解在 Java 的正则表达式中,两个 \ 代表其他语言中的一个 ****,这也就是为什么表示一位数字的正则表达式是 \d,而表示一个普通的反斜杠是 \\

字符 说明
\ 将下一字符标记为特殊字符、文本、反向引用或八进制转义符。例如,“n"匹配字符"n”。"\n"匹配换行符。序列"\\“匹配”\","\(“匹配”("。
^ 匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与"\n"或"\r"之后的位置匹配。
$ 匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与"\n"或"\r"之前的位置匹配。
* 零次或多次匹配前面的字符或子表达式。例如,zo* 匹配"z"和"zoo"。* 等效于 {0,}。
+ 一次或多次匹配前面的字符或子表达式。例如,"zo+"与"zo"和"zoo"匹配,但与"z"不匹配。+ 等效于 {1,}。
? 零次或一次匹配前面的字符或子表达式。例如,"do(es)?“匹配"do"或"does"中的"do”。? 等效于 {0,1}。
{ n} n 是非负整数。正好匹配 n 次。例如,"o{2}"与"Bob"中的"o"不匹配,但与"food"中的两个"o"匹配。
{ n,} n 是非负整数。至少匹配 n 次。例如,"o{2,}“不匹配"Bob"中的"o”,而匹配"foooood"中的所有 o。"o{1,}“等效于"o+”。"o{0,}“等效于"o*”。
{ n,m} mn 是非负整数,其中 n <= m。匹配至少 n 次,至多 m 次。例如,"o{1,3}"匹配"fooooood"中的头三个 o。‘o{0,1}’ 等效于 ‘o?’。注意:您不能将空格插入逗号和数字之间。
? 当此字符紧随任何其他限定符(*、+、?、{ n}、{ n,}、{ n,m})之后时,匹配模式是"非贪心的"。"非贪心的"模式匹配搜索到的、尽可能短的字符串,而默认的"贪心的"模式匹配搜索到的、尽可能长的字符串。例如,在字符串"oooo"中,"o+?“只匹配单个"o”,而"o+“匹配所有"o”。
. 匹配除"\r\n"之外的任何单个字符。若要匹配包括"\r\n"在内的任意字符,请使用诸如"[\s\S]"之类的模式。
(pattern) 匹配 pattern 并捕获该匹配的子表达式。可以使用 $0…$9 属性从结果"匹配"集合中检索捕获的匹配。若要匹配括号字符 ( ),请使用"(“或者”)"。
(?:pattern) 匹配 pattern 但不捕获该匹配的子表达式,即它是一个非捕获匹配,不存储供以后使用的匹配。这对于用"or"字符 (|) 组合模式部件的情况很有用。例如,'industr(?:y|ies) 是比 ‘industry|industries’ 更经济的表达式。
(?=pattern) 执行正向预测先行搜索的子表达式,该表达式匹配处于匹配 pattern 的字符串的起始点的字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,‘Windows (?=95|98|NT|2000)’ 匹配"Windows 2000"中的"Windows",但不匹配"Windows 3.1"中的"Windows"。预测先行不占用字符,即发生匹配后,下一匹配的搜索紧随上一匹配之后,而不是在组成预测先行的字符后。
(?!pattern) 执行反向预测先行搜索的子表达式,该表达式匹配不处于匹配 pattern 的字符串的起始点的搜索字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,‘Windows (?!95|98|NT|2000)’ 匹配"Windows 3.1"中的 “Windows”,但不匹配"Windows 2000"中的"Windows"。预测先行不占用字符,即发生匹配后,下一匹配的搜索紧随上一匹配之后,而不是在组成预测先行的字符后。
x|y 匹配 xy。例如,‘z|food’ 匹配"z"或"food"。’(z|f)ood’ 匹配"zood"或"food"。
[xyz] 字符集。匹配包含的任一字符。例如,"[abc]“匹配"plain"中的"a”。
[^xyz] 反向字符集。匹配未包含的任何字符。例如,"[^abc]“匹配"plain"中"p”,“l”,“i”,“n”。
[a-z] 字符范围。匹配指定范围内的任何字符。例如,"[a-z]"匹配"a"到"z"范围内的任何小写字母。
[^a-z] 反向范围字符。匹配不在指定的范围内的任何字符。例如,"[^a-z]"匹配任何不在"a"到"z"范围内的任何字符。
\b 匹配一个字边界,即字与空格间的位置。例如,“er\b"匹配"never"中的"er”,但不匹配"verb"中的"er"。
\B 非字边界匹配。“er\B"匹配"verb"中的"er”,但不匹配"never"中的"er"。
\cx 匹配 x 指示的控制字符。例如,\cM 匹配 Control-M 或回车符。x 的值必须在 A-Z 或 a-z 之间。如果不是这样,则假定 c 就是"c"字符本身。
\d 数字字符匹配。等效于 [0-9]。
\D 非数字字符匹配。等效于 [^0-9]。
\f 换页符匹配。等效于 \x0c 和 \cL。
\n 换行符匹配。等效于 \x0a 和 \cJ。
\r 匹配一个回车符。等效于 \x0d 和 \cM。
\s 匹配任何空白字符,包括空格、制表符、换页符等。与 [ \f\n\r\t\v] 等效。
\S 匹配任何非空白字符。与 [^ \f\n\r\t\v] 等效。
\t 制表符匹配。与 \x09 和 \cI 等效。
\v 垂直制表符匹配。与 \x0b 和 \cK 等效。
\w 匹配任何字类字符,包括下划线。与"[A-Za-z0-9_]"等效。
\W 与任何非单词字符匹配。与"[^A-Za-z0-9_]"等效。
\xn 匹配 n,此处的 n 是一个十六进制转义码。十六进制转义码必须正好是两位数长。例如,"\x41"匹配"A"。"\x041"与"\x04"&"1"等效。允许在正则表达式中使用 ASCII 代码。
\num 匹配 num,此处的 num 是一个正整数。到捕获匹配的反向引用。例如,"(.)\1"匹配两个连续的相同字符。
\n 标识一个八进制转义码或反向引用。如果 *n* 前面至少有 n 个捕获子表达式,那么 n 是反向引用。否则,如果 n 是八进制数 (0-7),那么 n 是八进制转义码。
\nm 标识一个八进制转义码或反向引用。如果 *nm* 前面至少有 nm 个捕获子表达式,那么 nm 是反向引用。如果 *nm* 前面至少有 n 个捕获,则 n 是反向引用,后面跟有字符 m。如果两种前面的情况都不存在,则 *nm* 匹配八进制值 nm,其中 nm 是八进制数字 (0-7)。
\nml n 是八进制数 (0-3),ml 是八进制数 (0-7) 时,匹配八进制转义码 nml
\un 匹配 n,其中 n 是以四位十六进制数表示的 Unicode 字符。例如,\u00A9 匹配版权符号 (©)。

根据 Java Language Specification 的要求,Java 源代码的字符串中的反斜线被解释为 Unicode 转义或其他字符转义。因此必须在字符串字面值中使用两个反斜线,表示正则表达式受到保护,不被 Java 字节码编译器解释。例如,当解释为正则表达式时,字符串字面值 “\b” 与单个退格字符匹配,而 “\b” 与单词边界匹配。字符串字面值 “(hello)” 是非法的,将导致编译时错误;要与字符串 (hello) 匹配,必须使用字符串字面值 “\(hello\)”。

Matcher 类的方法

索引方法

索引方法提供了有用的索引值,精确表明输入字符串中在哪能找到匹配:

序号 方法及说明
1 public int start() 返回以前匹配的初始索引。
2 public int start(int group) 返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引
3 public int end() 返回最后匹配字符之后的偏移量。
4 public int end(int group) 返回在以前的匹配操作期间,由给定组所捕获子序列的最后字符之后的偏移量。

查找方法

查找方法用来检查输入字符串并返回一个布尔值,表示是否找到该模式:

序号 方法及说明
1 public boolean lookingAt() 尝试将从区域开头开始的输入序列与该模式匹配。
2 public boolean find() 尝试查找与该模式匹配的输入序列的下一个子序列。
3 public boolean find(int start) 重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列。
4 public boolean matches() 尝试将整个区域与模式匹配。

替换方法

替换方法是替换输入字符串里文本的方法:

序号 方法及说明
1 public Matcher appendReplacement(StringBuffer sb, String replacement) 实现非终端添加和替换步骤。
2 public StringBuffer appendTail(StringBuffer sb) 实现终端添加和替换步骤。
3 public String replaceAll(String replacement) 替换模式与给定替换字符串相匹配的输入序列的每个子序列。
4 public String replaceFirst(String replacement) 替换模式与给定替换字符串匹配的输入序列的第一个子序列。
5 public static String quoteReplacement(String s) 返回指定字符串的字面替换字符串。这个方法返回一个字符串,就像传递给Matcher类的appendReplacement 方法一个字面字符串一样工作。

start 和 end 方法

下面是一个对单词 “cat” 出现在输入字符串中出现次数进行计数的例子:

import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class RegexMatches
{
     
    private static final String REGEX = "\\bcat\\b";
    private static final String INPUT =
                                    "cat cat cat cattie cat";
 
    public static void main( String args[] ){
     
       Pattern p = Pattern.compile(REGEX);
       Matcher m = p.matcher(INPUT); // 获取 matcher 对象
       int count = 0;
 
       while(m.find()) {
     
         count++;
         System.out.println("Match number "+count);
         System.out.println("start(): "+m.start());
         System.out.println("end(): "+m.end());
      }
   }
}

以上实例编译运行结果如下:

# Match number 1
# start(): 0
# end(): 3
# Match number 2
# start(): 4
# end(): 7
# Match number 3
# start(): 8
# end(): 11
# Match number 4
# start(): 19
# end(): 22

可以看到这个例子是使用单词边界,以确保字母 “c” “a” “t” 并非仅是一个较长的词的子串。它也提供了一些关于输入字符串中匹配发生位置的有用信息。

Start 方法返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引,end 方法最后一个匹配字符的索引加 1。

matches 和 lookingAt 方法

matches 和 lookingAt 方法都用来尝试匹配一个输入序列模式。它们的不同是 matches 要求整个序列都匹配,而lookingAt 不要求。

lookingAt 方法虽然不需要整句都匹配,但是需要从第一个字符开始匹配。

这两个方法经常在输入字符串的开始使用。

我们通过下面这个例子,来解释这个功能:

import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class RegexMatches
{
     
    private static final String REGEX = "foo";
    private static final String INPUT = "fooooooooooooooooo";
    private static final String INPUT2 = "ooooofoooooooooooo";
    private static Pattern pattern;
    private static Matcher matcher;
    private static Matcher matcher2;
 
    public static void main( String args[] ){
     
       pattern = Pattern.compile(REGEX);
       matcher = pattern.matcher(INPUT);
       matcher2 = pattern.matcher(INPUT2);
 
       System.out.println("Current REGEX is: "+REGEX);
       System.out.println("Current INPUT is: "+INPUT);
       System.out.println("Current INPUT2 is: "+INPUT2);
 
 
       System.out.println("lookingAt(): "+matcher.lookingAt());
       System.out.println("matches(): "+matcher.matches());
       System.out.println("lookingAt(): "+matcher2.lookingAt());
   }
}

以上实例编译运行结果如下:

Current REGEX is: foo
Current INPUT is: fooooooooooooooooo
Current INPUT2 is: ooooofoooooooooooo
lookingAt(): true
matches(): false
lookingAt(): false

replaceFirst 和 replaceAll 方法

replaceFirst 和 replaceAll 方法用来替换匹配正则表达式的文本。不同的是,replaceFirst 替换首次匹配,replaceAll 替换所有匹配。

下面的例子来解释这个功能:

import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class RegexMatches
{
     
    private static String REGEX = "dog";
    private static String INPUT = "The dog says meow. " +
                                    "All dogs say meow.";
    private static String REPLACE = "cat";
 
    public static void main(String[] args) {
     
       Pattern p = Pattern.compile(REGEX);
       // get a matcher object
       Matcher m = p.matcher(INPUT); 
       INPUT = m.replaceAll(REPLACE);
       System.out.println(INPUT);
   }
}

以上实例编译运行结果如下:

The cat says meow. All cats say meow.

appendReplacement 和 appendTail 方法

Matcher 类也提供了appendReplacement 和 appendTail 方法用于文本替换:

看下面的例子来解释这个功能:

import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class RegexMatches
{
     
   private static String REGEX = "a*b";
   private static String INPUT = "aabfooaabfooabfoobkkk";
   private static String REPLACE = "-";
   public static void main(String[] args) {
     
      Pattern p = Pattern.compile(REGEX);
      // 获取 matcher 对象
      Matcher m = p.matcher(INPUT);
      StringBuffer sb = new StringBuffer();
      while(m.find()){
     
         m.appendReplacement(sb,REPLACE);
      }
      m.appendTail(sb);
      System.out.println(sb.toString());
   }
}

以上实例编译运行结果如下:

-foo-foo-foo-kkk

PatternSyntaxException 类的方法

PatternSyntaxException 是一个非强制异常类,它指示一个正则表达式模式中的语法错误。

PatternSyntaxException 类提供了下面的方法来帮助我们查看发生了什么错误。

序号 方法及说明
1 public String getDescription() 获取错误的描述。
2 public int getIndex() 获取错误的索引。
3 public String getPattern() 获取错误的正则表达式模式。
4 public String getMessage() 返回多行字符串,包含语法错误及其索引的描述、错误的正则表达式模式和模式中错误索引的可视化指示。

第14章 类型信息(包含Java反射机制)

​ 运行时类型信息使我们可以在程序运行时发现和使用类型信息。Java有两种方式让我们在运行时识别对象和类的信息:

  • RTTI(Run Time Type Identification):在运行时识别一个对象的类型,它假定我们在编译时已经知道了所有的类型。
  • 反射机制:它允许我们在运行时发现和使用类的信息。

14.1 为什么需要RTTI

​ RTTI能够保证我们的类型转换。在面向对象程序设计中,我们通常的做法是:让代码只操纵对基类的引用。这样的话,再添加一个新类来扩展程序,则不会影响原来的代码。 例如:

abstract class Shape {
     
    void draw() {
     
        System.out.println(this + ".draw()");
    }
    abstract public String toString();
}
 
class Circle extends Shape {
     
    public String toString() {
      return "Circle"; }
}
class Square extends Shape {
     
    public String toString() {
      return "Square"; }
}
class Triangle extends Shape {
     
    public String toString() {
      return "Triangle"; }
}
 
public class Shapes {
     
    public static void main(String[] args) {
     
        List<Shape> shapeList = Arrays.asList(new Circle(), new Square(), new Triangle());
        for (Shape shape : shapeList) {
     
            shape.draw();
        }
    }
}

​ 当将具体元素(Circle,Square,Triangle)放入List容器中时,会发生向上转型,并且会丢失元素具体类型。 List容器实际上将所有事物都当成Object持有,并且在元素取出时,会自动转型为Shape。

​ 在这个例子中,RTTI类转换并不彻底:Object转型为Shape,而不是具体的Circle,Square或Triangle。因为在编译时,由容器和Java的泛型确保List中保存的是Shape,运行时由类型转换操作来确保这一点。

​ 接下来就是多态机制:即Shape对象具体执行什么代码,是由引用所指向的具体对象(Circle,Square,Triangle)而决定的。优点是:容易编写、阅读和维护,设计上容易实现、理解和改变。

14.2 Class对象

​ 在Java中,类型信息由Class对象保存,该对象就是用来创建类的所有常规对象的。

​ 类是程序的一部分,每个类都有一个Class对象。即每编写且编译了一个新类,就会产生一个Class对象(保存在一个同名的.class文件中)。Java虚拟机(JVM)通过使用类加载器子系统,可以生成类的Class对象。

​ 类加载器子系统实际上可以包含一条类加载器链,但是只有一个原生类加载器,它是JVM实现的一部分。原生加载器加载的是可信类,包括Java API类,它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,如果你有特殊需求(例如以某种特殊的方式加载类,以支持Web服务器应用,或者再网络中下载类),那么你有一种方式可以挂接额外的类加载器。

​ 所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序创建第一个对类的静态成员引用时,就会加载这个类。因此,Java程序在它开始运行之前并非全部被加载,其各个部分是在必需时才加载的

​ 在程序运行过程中,如果使用到某个类,类加载器首先检查这个类的Class对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名查找.class文件。

在运行时,我们获取类型信息一般有两种方式:

  • CLass.forName(String):通过Class类的静态方法,使用全限定类名获取。
  • Object.getClass():通过根基类Object的方法,获取其Class对象。

Class对象包含了很多有用的方法,下面是其中的一部分:

interface HasBatteries {
     }
interface Waterproof {
     }
interface Shoots {
     }
 
class Toy {
     
    Toy() {
     }
    Toy(int i) {
     }
}
class FancyToy extends Toy implements HasBatteries,Waterproof,Shoots {
     
    FancyToy() {
      super(1); }
}
 
public class ToyTest {
     
    static void printInfo(Class cc) {
     
        print("Class name: " + cc.getName() + 
            " is interface? [" + cc.isInterface() + "]");
        print("Simple name: " + cc.getSimpleName());
        print("Canonical name : " + cc.getCanonicalName());
    }
    
    public static void main(String[] args) {
     
        Class c = null;
        try {
     
            c = Class.forName("com.jiao.thinkInJava.example.FancyToy");
        } catch (ClassNotFoundException e) {
     
            print("Can't find FancyToy");
            System.exit(1);
        }
        printInfo(c);
        for (Class face : c.getInterfaces()) 
            printInfo(face);
        Class up = c.getSuperclass();
        Object obj = null;
        try {
     
            obj = up.newInstance();
        } catch (InstantiationException e) {
     
            print("Cannot instantiate");
            System.exit(1);
        } catch (IllegalAccessException e) {
     
            print("Cannot access");
            System.exit(1);
        }
        printInfo(obj.getClass());
    }
}

上例中使用了以下几个方法:

  • String getName():获取全限定类名
  • String getSimpleName():获取不包含包名的类名
  • String getCanonicalName():获取全限定类名
  • boolean isInterface():该Class对象是否表示某个接口
  • Class[] getInterfaces():获取该Class对象中所包含的接口
  • Class getSuperclass():获取该Class对象的基类Class对象
  • newInstance():使用Class对象通过默认构造器创建该类实例

14.2.1 类字面常量

​ Java还提供了另一种方式来生成对Class对象的引用,即类字面常量:

FancyToy.class

​ 这样做会在编译时就受到检查,简单、高效。并且,类字面常量不仅可以应用于普通的类,也可应用于接口、数组以及基本数据类型。另外,对于基本数据类型的包装器类,还有一个标准字段TYPE,其指向对应的基本数据类型的Class对象,例如:

  • boolean.class 等价于 Boolean.TYPE
  • char.class 等价于 Character.TYPE

为了使用类而做的准备工作实际包含三个步骤:

  • 加载:由类加载器执行,通过查找字节码,从而创建一个Class对象。
  • 连接:验证字节码并为静态域分配存储空间,在必需的情况下,解析该类所创建的其他类的引用。
  • 初始化:如果该类具有超类,则对其初始化,执行静态初始化器和初始化块。

而使用.class来创建Class对象的引用时,不会自动初始化该Class对象,初始化被延迟到了对静态方法或非常数静态域进行首次引用时才执行:

class Initable {
     
    static final int staticFinal = 66;
    static final int staticFinal2 = ClassInitialization.random.nextInt(1000);
    static {
      System.out.println("Initializing Initable"); }
}
class Initable2 {
     
    static int staticNotFinal = 99;
    static {
      System.out.println("Initializing Initable2"); }
}
class Initable3 {
     
    static int staticNotFinal = 33;
    static {
      System.out.println("Initializing Initable3"); }
}
public class ClassInitialization {
     
    public static Random random = new Random(66);
    public static void main(String[] args) throws ClassNotFoundException {
     
        Class initable = Initable.class;
        System.out.println("After creating Initable ref");
        System.out.println(Initable.staticFinal);
        System.out.println(Initable.staticFinal2);
        System.out.println(Initable2.staticNotFinal);
        Class initable3 = Class.forName("com.jiao.thinkInJava.example.Initable3");
        System.out.println("After creating Initable3 ref");
        System.out.println(Initable3.staticNotFinal);
    }
}

我们发现:

  • 访问某个类的编译期常量时,不会初始化该类。
  • 访问某个类的非编译期常量时,总要求其在被读取前,要先进行链接(为该域分配存储空间)和初始化(初始化该存储空间)。

14.2.2 泛化的Class引用

Java SE5中Class类引入了对泛型的支持:

public class GenericClassReferences {
     
    public static void main(String[] args) {
     
         Class intClass = int.class;
         Class<Integer> genericIntClass = int.class;
         genericIntClass = Integer.class;
         intClass = double.class;
         //! genericIntClass = double.class;
    }
}

​ 可以发现,普通的类引用可以被重新赋值给任何其他类型的Class对象,而泛型类引用只能指向其声明的类型。即:使用泛型语法可以让编译器强制执行额外的类型检查。

​ 为了使编译器放松对泛化Class的类型限制,可以使用通配符?,它表示任何事物:

public class WildcardClassReferences {
     
    public static void main(String[] args) {
     
         Class<?> intClass = int.class;
         intClass = double.class;
    }
}

​ 如果需要将Class引用所指向对象类型限定为某种类型,或该类型的任何子类型,则需要将通配符与extends关键字相结合:

public class BoundedClassReferences {
     
    public static void main(String[] args) {
     
        Class<? extends Number> bounded = int.class;
        bounded = double.class;
        bounded = Number.class;
    }
}

​ 向Class引用添加泛型语法的原因仅仅是为了提供编译期类型检查,而使用普通Class引用,则只能在运行时才会发现错误。

下面的示例使用泛型类语法,填充一个List:

class CountedInteger {
     
    private static long counter;
    private final long id = counter++;
    public String toString() {
      return Long.toString(id); }
}
 
public class FilledList <T> {
     
    private Class<T> type;
    public FilledList(Class<T> type) {
      this.type = type; }
    public List<T> create(int n) {
     
        List<T> result = new ArrayList<T>();
        try {
     
            for (int i = 0; i < n; i++) 
                result.add(type.newInstance());
        } catch (Exception e) {
     
            throw new RuntimeException(e);
        }
        return result;
    }
    public static void main(String[] args) {
     
        FilledList<CountedInteger> fl = new FilledList<CountedInteger>(CountedInteger.class);
        System.out.println(fl.create(15));
    }
}

并且,当我们将泛型语法用于Class对象时,newInstance()将返回该对象的确切类型:

public class GenericToyTest {
     
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
     
        Class<FancyToy> ftClass = FancyToy.class;
        FancyToy fancyToy = ftClass.newInstance();
        Class<? super FancyToy> up = ftClass.getSuperclass();
        Object obj = up.newInstance();
    }

表达式表示FancyToy的父类。

14.2.3 新的转型语法

Java SE5还添加了用于Class引用的转型语法:

class Building {
     }
class House extends Building {
     }
 
public class ClassCasts {
     
    public static void main(String[] args) {
     
        Building b = new Building();
        Class<House> houseType = House.class;
        House h = houseType.cast(b);
        h = (House) b;
    }
}

Class.cast(Object)方法接收一个对象,并将其转型为Class引用的类型。我们发现,它和强制转型所实现的功能一样,但更加麻烦。

14.3 类型转换前先做检查

Java中的RTTI有三种形式:

  • 传统的类型转换:由编译器检查转型是否合理。
  • Class对象:通过该对象获取运行时所需的类型信息。
  • instanceof:用于判断指定对象是否是某个特定类型的实例。

下面是继承自Individual的类继承体系:

public class Person extends Individual {
     
    public Person(String name) {
      super(name); }
}
public class Pet extends Individual {
     
    public Pet() {
      super(); }
    public Pet(String name) {
      super(name); }
}
 
public class Dog extends Pet {
     
    public Dog() {
      super(); }
    public Dog(String name) {
      super(name); }
}
public class Mutt extends Dog {
     
    public Mutt() {
      super(); }
    public Mutt(String name) {
      super(name); }
}
public class Pug extends Dog {
     
    public Pug() {
      super(); }
    public Pug(String name) {
      super(name); }
}
 
public class Cat extends Pet {
     
    public Cat() {
      super(); }
    public Cat(String name) {
      super(name); }
}
public class EgyptianMau extends Cat {
     
    public EgyptianMau() {
      super(); }
    public EgyptianMau(String name) {
      super(name); }
}
public class Manx extends Cat {
     
    public Manx() {
      super(); }
    public Manx(String name) {
      super(name); }
}
public class Cymric extends Manx {
     
    public Cymric() {
      super(); }
    public Cymric(String name) {
      super(name); }
}
 
public class Rodent extends Pet {
     
    public Rodent() {
      super(); }
    public Rodent(String name) {
      super(name); }
}
public class Rat extends Rodent {
     
    public Rat() {
      super(); }
    public Rat(String name) {
      super(name); }
}
public class Mouse extends Rodent {
     
    public Mouse() {
      super(); }
    public Mouse(String name) {
      super(name); }
}
public class Hamster extends Rodent {
     
    public Hamster() {
      super(); }
    public Hamster(String name) {
      super(name); }
}

下面是一个能够随机创建不同类型宠物的抽象类:

public abstract class PetCreator {
     
    private Random random = new Random(47);
    public abstract List<Class<? extends Pet>> types();
    
    public Pet randomPet() {
     
        int n = random.nextInt(types().size());
        try {
     
            return types().get(n).newInstance();
        } catch (InstantiationException e) {
     
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
     
            throw new RuntimeException(e);
        }
    }
    public Pet[] createArray(int size) {
     
        Pet[] result = new Pet[size];
        for (int i = 0; i < size; i++) 
            result[i] = randomPet();
        return result;
    }
    public ArrayList<Pet> arrayList(int size) {
     
        ArrayList<Pet> result = new ArrayList<Pet>();
        Collections.addAll(result, createArray(size));
        return result;
    }
}

其中,抽象方法types()用于获取由Class对象构成的List。下面是使用forName()的一个具体实现:

public class ForNameCreator extends PetCreator {
     
    
    private static List<Class<? extends Pet>> types = new ArrayList<Class<? extends Pet>>();
    private static String[] typeNames = {
     
        "typeinfo.pets.Mutt",
        "typeinfo.pets.Pug",
        "typeinfo.pets.EgyptianMau",
        "typeinfo.pets.Manx",
        "typeinfo.pets.Cymric",
        "typeinfo.pets.Rat",
        "typeinfo.pets.Mouse",
        "typeinfo.pets.Hamster"
    };
    static {
      loader(); }
    private static void loader() {
     
        try {
     
            for (String name : typeNames) 
                types.add((Class<? extends Pet>)Class.forName(name));
        } catch (Exception e) {
     
            throw new RuntimeException(e);
        }
    }
    
    public List<Class<? extends Pet>> types() {
      return types; }
    
}

下面,我们就可以使用instanceof来对Pet进行计数了:

public class PetCount {
     
    static class PetCounter extends HashMap<String, Integer> {
     
        public void count(String type) {
     
            Integer quantity = get(type);
            if(quantity == null)
                put(type, 1);
            else
                put(type, quantity + 1); 
        }
    }
    
    public static void countPets(PetCreator creator) {
     
        PetCounter counter = new PetCounter();
        for (Pet pet : creator.createArray(20)) {
     
            System.out.print(pet.getClass().getSimpleName() + " ");
            if(pet instanceof Pet) counter.count("Pet");
            if(pet instanceof Dog) counter.count("Dog");
            if(pet instanceof Mutt) counter.count("Mutt");
            if(pet instanceof Pug) counter.count("Pug");
            if(pet instanceof Cat) counter.count("Cat");
            if(pet instanceof Manx) counter.count("Manx");
            if(pet instanceof EgyptianMau) counter.count("EgyptianMau");
            if(pet instanceof Cymric) counter.count("Cymric");
            if(pet instanceof Rodent) counter.count("Rodent");
            if(pet instanceof Rat) counter.count("Rat");
            if(pet instanceof Mouse) counter.count("Mouse");
            if(pet instanceof Hamster) counter.count("Hamster");
        }
        System.out.println();
        System.out.println(counter);
    }
    public static void main(String[] args) {
     
        countPets(new ForNameCreator());
    }
}

instanof只能用于对象与具体类型进行判断,而无法将对象与Class对象作比较。 上述程序中,大量的instanceof使我们的代码出现了冗余。

14.3.1 使用类字面常量

下面,我们使用类字面常量重新实现PetCount:

public class LiteralPetCreator extends PetCreator {
     
    public static final List<Class<? extends Pet>>  allTypes = 
                Collections.unmodifiableList(
                Arrays.asList(Pet.class, Dog.class, Cat.class, Rodent.class,
                        Mutt.class, Pug.class, EgyptianMau.class, Manx.class,
                        Cymric.class, Rat.class, Mouse.class, Hamster.class));
 
    private static final List<Class<? extends Pet>> types = 
                    allTypes.subList(allTypes.indexOf(Mutt.class), allTypes.size());
 
    public List<Class<? extends Pet>> types() {
     
        return types;
    }
    public static void main(String[] args) {
     
        System.out.println(types);
    }
}

为了将该方式作为默认实现,我们使用代理模式,将LiteralPetCreator类包装起来:

public class Pets {
     
    public static final PetCreator creator = new LiteralPetCreator();
 
    public static Pet randomPet() {
      return creator.randomPet(); }
    public static Pet[] createArray(int size) {
      return creator.createArray(size); }
    public static ArrayList<Pet> arrayList(int size) {
      return creator.arrayList(size); }
}

现在,我们可以对其进行测试了:

public class PetCount2 {
     
    public static void main(String[] args) {
     
        PetCount.countPets(Pets.creator);
    }
}

可以发现,其输出与PetCount.java完全相同。

14.3.2 动态的instanceof

​ 在之前的例子中,大量的instanceof使得代码变得冗余,而Class.isInstance()方法通过动态测试对象的途径解决了上述问题:

public class PetCount3 {
     
    static class PetCounter extends LinkedHashMap<Class<? extends Pet>, Integer> {
     
        public PetCounter() {
      super(MapData.map(LiteralPetCreator.allTypes, 0)); }
        
        public void count(Pet pet) {
     
            for (Map.Entry<Class<? extends Pet>, Integer> pair : entrySet()) 
                if(pair.getKey().isInstance(pet))
                    put(pair.getKey(), pair.getValue() + 1);
        }
        public String toString() {
     
            StringBuilder result = new StringBuilder("{");
            for (Map.Entry<Class<? extends Pet>, Integer> pair : entrySet()) 
                result.append(pair.getKey().getSimpleName())
                      .append("=")
                      .append(pair.getValue())
                      .append(", ");
            result.delete(result.length()-2, result.length());
            result.append("}");
            return result.toString();
        }
    }
    
    public static void main(String[] args) {
     
        PetCounter petCounter = new PetCounter();
        for (Pet pet : Pets.createArray(20)) {
     
            System.out.print(pet.getClass().getSimpleName() + " ");
            petCounter.count(pet);
        }
        System.out.println();
        System.out.println(petCounter);
    }
}

可以看到,isInstance()方法使我们的代码变得简洁。并且,如果要添加新类型的Pet,只需简单地改变LiteralPetCreator.java中的常量数组即可。

14.3.3 递归计数

​ 在上述代码中,我们使用Map预加载了所有不同的Pet类,但其局限于Pet类。下面,我们创建一个通用的计数工具:

public class TypeCounter extends HashMap<Class<?>, Integer>{
     
    private Class<?> baseType;
    public TypeCounter(Class<?> baseType) {
     
        this.baseType = baseType;
    }
    public void count(Object obj) {
     
        Class<?> type = obj.getClass();
        if(!baseType.isAssignableFrom(type))
            throw new RuntimeException(obj + " incorrect type: " 
                    + type +", should be type or subtype of " + baseType);
        countClass(type);
    }
    
    private void countClass(Class<?> type) {
     
        Integer quantity = get(type);
        put(type, quantity == null ? 1 :quantity+1);
        Class<?> superClass = type.getSuperclass();
        if(superClass != null && baseType.isAssignableFrom(superClass))
            countClass(superClass);
    }
    public String toString() {
     
        StringBuilder result = new StringBuilder("{");
        for (Map.Entry<Class<?>, Integer> pair : entrySet()) 
            result.append(pair.getKey().getSimpleName())
                  .append("=")
                  .append(pair.getValue())
                  .append(", ");
        result.delete(result.length()-2, result.length());
        result.append("}");
        return result.toString();
    }
}

我们通过isAssignableFrom()方法来执行运行时的检查,以校验所传递的对象是否属于我们所指定的继承结构。如果该类型含有父类,则将其父类进行递归计数。

下面,通过测试证明该通用计数工具:

public class PetCount4 {
     
    public static void main(String[] args) {
     
        TypeCounter counter = new TypeCounter(Pet.class);
        for (Pet pet : Pets.createArray(20)) {
     
            System.out.print(pet.getClass().getSimpleName() + " ");
            counter.count(pet);
        }
        System.out.println();
        System.out.println(counter);
    }
}

14.4 注册工厂

​ 我们生成Pet继承结构中的对象时,存在一个问题:每次向该继承结构添加新的Pet类型时,必须将其添加进LiteralPetCreator.java中的成员List中。其原因是:硬编码。

​ 解决硬编码的方式有很多:将信息存入文件或数据库等。而如果不得不硬编码,最佳做法是:将该列表置于一个位于中心、位置明显的地方,即继承结构的基类。

​ 下面,我们通过使用工厂方法设计模式,将对象的创建工作交给类自己完成:

public interface Factory <T> {
      T create(); }
 
class Part {
     
    public String toString() {
      return getClass().getSimpleName(); }
    static List<Factory<? extends Part>> partFactories = new ArrayList<Factory<? extends Part>>();
    static {
     
        partFactories.add(new FuelFilter.Factory());
        partFactories.add(new AirFilter.Factory());
        partFactories.add(new CabinAirFilter.Factory());
        partFactories.add(new OilFilter.Factory());
        partFactories.add(new FanBelt.Factory());
        partFactories.add(new PowerSteeringBelt.Factory());
        partFactories.add(new GeneratorBelt.Factory());
    }
    private static Random random = new Random(66);
    public static Part createRandom() {
     
        int n = random.nextInt(partFactories.size());
        return partFactories.get(n).create();
    }
}
 
class Filter extends Part {
     }
class FuelFilter extends Filter {
     
    public static class Factory implements com.jiao.thinkInJava.test.typeinfo.pets.Factory<FuelFilter> {
     
        public FuelFilter create() {
      return new FuelFilter(); }
    }
}
class AirFilter extends Filter {
     
    public static class Factory implements com.jiao.thinkInJava.test.typeinfo.pets.Factory<AirFilter> {
     
        public AirFilter create() {
      return new AirFilter(); }
    }
}
class CabinAirFilter extends Filter {
     
    public static class Factory implements com.jiao.thinkInJava.test.typeinfo.pets.Factory<CabinAirFilter> {
     
        public CabinAirFilter create() {
      return new CabinAirFilter(); }
    }
}
class OilFilter extends Filter {
     
    public static class Factory implements com.jiao.thinkInJava.test.typeinfo.pets.Factory<OilFilter> {
     
        public OilFilter create() {
      return new OilFilter(); }
    }
}
 
class Belt extends Part {
     }
class FanBelt extends Belt {
     
    public static class Factory implements com.jiao.thinkInJava.test.typeinfo.pets.Factory<FanBelt> {
     
        public FanBelt create() {
      return new FanBelt(); }
    }
}
class GeneratorBelt extends Belt {
     
    public static class Factory implements com.jiao.thinkInJava.test.typeinfo.pets.Factory<GeneratorBelt> {
     
        public GeneratorBelt create() {
      return new GeneratorBelt(); }
    }
}
class PowerSteeringBelt extends Belt {
     
    public static class Factory implements com.jiao.thinkInJava.test.typeinfo.pets.Factory<PowerSteeringBelt> {
     
        public PowerSteeringBelt create() {
      return new PowerSteeringBelt(); }
    }
}
 
public class RegisteredFactory {
     
    public static void main(String[] args) {
     
        for (int i = 0; i < 10; i++) 
            Part.createRandom();
    }
}

14.5 instanceof与Class的等价性

​ 在查询类型信息时,以instanceof的形式与直接比较Class对象有一个很重要的差别:

class Base {
     }
class Derived extends Base {
     }
 
public class FamilyVsExactType {
     
    static void test(Object x) {
     
        print("Testing x of type " + x.getClass());
        print("x instanceof Base " + (x instanceof Base));
        print("x instanceof Derived " + (x instanceof Derived));
        print("Base.isInstance(x) " + Base.class.isInstance(x));
        print("Derived.isInstance(x) " + Derived.class.isInstance(x));
        print("x.getClass() == Base.class " + (x.getClass() == Base.class));
        print("x.getClass() == Derived.class " + (x.getClass() == Base.class));
        print("x.getClass().equals(Base.class) " + (x.getClass().equals(Base.class)));
        print("x.getClass().equals(Derived.class) " + (x.getClass().equals(Derived.class)));
    }
}

通过测试发现:

  • instanceof和isInstance()的结果是一致的,==和equals()的结果是一致的。
  • instanceof:该对象的类型是指定类型或该类型子类,均返回true。
  • ==:该对象的类型只有和指定类型完全一致时,才返回true。

14.6 反射:运行时的类型信息

​ 使用RTTI获取类型信息有一个限制:在编译时,编译器必须知道所有要通过RTTI来处理的类。

​ 但如果获取了一个指向某个并不在当前程序空间的对象的引用,那么在编译时则无法获知这个对象所属的类。例如,假设从磁盘文件,或者网络连接中获取了一串字节码,并被告知这些字节码代表了一个类。

​ 我们想要在运行时获取类的信息的另一个动机,便是希望提供在跨网络的远程平台上创建和运行对象的能力。即远程方法调用(RMI):它允许一个Java程序将对象分布到多台机器上。

​ 反射机制则可以满足我们上述需求。Class类与java.lang.reflect类库一起对反射进行了支持,该类库包含了Field、Method以及Constructor类。

注意,RTTI和反射之间真正的区别在于:

  • RTTI:编译器在编译时打开和检查.class文件。
  • 反射:.class文件在编译时无法获取,是在运行时打开和检查.class文件。

14.6.1 类方法提取器

​ 在我们需要创建更加动态的代码时,反射会体现其价值。在Java中,反射被用来支持其他特性,如对象序列化和JavaBean。

下面的例子使用反射动态提取某个类的信息,进而展示该类中的所有方法:

//Args:ShowMethods
 
public class ShowMethods {
     
    public static String usage = 
            "usage:\n" + 
            "ShowMethods qualified.class.name\n" + 
            "To show all methods in class or:\n" +
            "ShowMethods qualified.class.name word\n" +
            "To search for methods involving 'word'";
    private static Pattern pattern = Pattern.compile("\\w+\\.");
    public static void main(String[] args) {
     
        if(args.length < 1) {
     
            System.out.println(usage);
            System.exit(0);
        }
        int lines = 0 ;
        try {
     
            Class<?> c = Class.forName(args[0]);
            Method[] methods = c.getMethods();
            Constructor<?>[] constructors = c.getConstructors();
            if(args.length == 1) {
     
                for (Method method : methods) 
                    print(pattern.matcher(method.toString()).replaceAll(""));
                for (Constructor ctor : constructors) 
                    print(pattern.matcher(ctor.toString()).replaceAll(""));
                lines = methods.length + constructors.length;
            } else {
     
                for (Method method : methods) 
                    if(method.toString().indexOf(args[1]) != -1)    {
     
                        print(pattern.matcher(method.toString()).replaceAll(""));
                        lines ++;
                    }
                for (Constructor ctor : constructors) 
                    if(ctor.toString().indexOf(args[1]) != -1)  {
     
                        print(pattern.matcher(ctor.toString()).replaceAll(""));
                        lines ++;
                    }
            }
        } catch (Exception e) {
     
            print("No such class: " + e);
        }
    }
}

上例中使用了以下方法:

  • Class.getMethods():返回Method数组,从而通过Method对象获取与方法相关的信息。
  • Class.getConstructors():返回Constructor数组,从而通过Constructor获取与构造器相关的信息。

由于Class.forName()所产生的结果在编译期是不可知的,因此所有的方法特征签名信息都是在执行时被提取出来的。反射的强大作用在于:它能够创建一个在编译时完全未知的对象,并调用此对象的方法。

14.7 动态代理

​ 代理是基本的设计模式之一,通常情况下,我们使用代理对象代替真实对象,从而提供额外的行为。下面是一个用来展示代理结构的简单示例:

interface Interface {
     
    void doSomething();
    void somethingElse(String arg);
}
 
class RealObject implements Interface {
     
    public void doSomething() {
      print("doSomething"); }
    public void somethingElse(String arg) {
      print("somethingElse " + arg); }
}
 
class SimpleProxy implements Interface {
     
    private Interface proxied;
    public SimpleProxy(Interface proxied) {
     
        this.proxied = proxied;
    }
    
    public void doSomething() {
      
        print("SimpleProxy doSomething");
        proxied.doSomething(); 
    }
    public void somethingElse(String arg) {
     
        print("SimpleProxy somethingElse " + arg);
        proxied.somethingElse(arg); 
    }
}
public class SimpleProxyDemo {
     
    public static void consumer (Interface iface) {
     
        iface.doSomething();
        iface.somethingElse("bonobo");
    }
    public static void main(String[] args) {
     
        consumer(new RealObject());
        consumer(new SimpleProxy(new RealObject()));
    }
}

Java的动态代理比代理的思想更向前迈进了一步,它可以动态地创建代理并动态地处理对所代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器上。下面使用了动态代理重写了上例:

class DynamicProxyHandler implements InvocationHandler{
     
    private Object proxied;
    public DynamicProxyHandler(Object proxied) {
     
        this.proxied = proxied;
    }
 
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
     
        System.out.println("**** proxy: " + proxy.getClass() 
                + ", method: " + method + ", args: " + args);
        if(args != null )
            for (Object arg : args) 
                System.out.println("  " + arg);
        return method.invoke(proxied, args);
    }
 
}
 
public class SimpleDynamicProxy {
     
    public static void consumer(Interface iface) {
     
        iface.doSomething();
        iface.somethingElse("bonobo");
    }
    public static void main(String[] args) {
     
        RealObject real = new RealObject();
        consumer(real);
        Interface proxy = (Interface) Proxy.newProxyInstance(
                Interface.class.getClassLoader(),
                new Class[]{
      Interface.class },
                new DynamicProxyHandler(real));
        consumer(proxy);
    }
}

通过静态方法Proxy.newProxyInstance()可以创建动态代理对象,其需要三个参数:

  • ClassLoader:类加载器,通常从已被加载的类中获取其类加载器。
  • Class[]:希望该代理实现的接口列表。
  • InvocationHandler:InvocationHandler接口的一个实现类。

通常在执行代理操作时,需要使用Method.invoke()将请求转发给被代理的对象,并传入必需的参数。并且,我们可以通过方法的信息,过滤我们所希望加强的方法:

class MethodSelector implements InvocationHandler {
     
    private Object proxied;
    public MethodSelector(Object proxied) {
     
        this.proxied = proxied;
    }
 
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
     
        if(method.getName().equals("interesting"))
            print("Proxy detected the interesting method");
        return method.invoke(proxied, args);
    }
}
 
interface SomeMethods {
     
    void boring1();
    void boring2();
    void boring3();
    void interesting(String arg);
}
 
class Implementation implements SomeMethods {
     
    public void boring1() {
      print("boring1"); }
    public void boring2() {
      print("boring2"); }
    public void boring3() {
      print("boring3"); }
    public void interesting(String arg) {
      print("interesting " + arg ); }
}
 
public class SelectingMethods{
     
    public static void main(String[] args) {
     
        SomeMethods proxy = (SomeMethods) Proxy.newProxyInstance(
                SomeMethods.class.getClassLoader(), 
                new Class[]{
     SomeMethods.class},
                new MethodSelector(new Implementation()));
        proxy.boring1();
        proxy.boring2();
        proxy.boring3();
        proxy.interesting("bonobo");
    }
}

14.8 空对象

​ 当我们使用内置的null表示缺少对象时,经常需要在使用前对其判断,否则将会产生异常。有时引入空对象的思想将会有所用途:它可以接受传递给它所代表对象的所有消息,但其返回却不具实际意义。

​ 通过这种方式,我们可以假设所有的对象都是有效的,而不必浪费编程精力去检查null。即使空对象可以响应实际对象的所有消息,我们仍需要某种方式去测试其是否为空,而最简单的方式就是创建一个标记接口:

public interface Null {
     }

​ 这使得我们可以使用intanceof探测空对象,并且不必在所有类中都添加isNull()方法 了。

​ 很多系统都会有一个Person类,很多情况我们没有一个实际的人或不具备这个人的全部信息,如公司正在招聘的某个岗位:

public class Person {
     
    public final String first;
    public final String last;
    public final String address;
    public Person(String first, String last, String address) {
     
        this.first = first;
        this.last = last;
        this.address = address;
    }
    public String toString() {
     
        return "Person: " + first + " " + last + " " + address ;
    }
    
    public static class NullPerson extends Person implements Null {
     
        private NullPerson() {
      super("None", "None", "None"); }
        public String toString() {
      return "NullPerson"; }
    }
    public static final Person NULL = new NullPerson();
}

空对象一般是不可变的,我们将其构造器设为private,并将该对象设置为静态final,使成为单例。

下面的Position类封装了岗位名和与之对应的人的信息:

public class {
     
    private String title;
    private Person person;
    public Position(String title, Person person) {
     
        this.title = title;
        this.person = person;
        if(person == null)
            person = Person.NULL;
    }
    public Position(String title) {
     
        this.title = title;
        person = Person.NULL;
    }
    public String getTitle() {
     
        return title;
    }
    public void setTitle(String title) {
     
        this.title = title;
    }
    public Person getPerson() {
     
        return person;
    }
    public void setPerson(Person person) {
     
        this.person = person;
        if(person == null)
            person = Person.NULL;
    }
    public String toString() {
     
        return "Position: " + title + " " + person ;
    }
}

接下来,我们可以使用Staff类填充职位:

public class Staff extends ArrayList<Position>{
     
    public void add(String title,Person person) {
     
        add(new Position(title,person));
    }
    public void add(String... titles) {
     
        for (String title : titles) 
            add(new Position(title));
    }
    public Staff(String... titles) {
      add(titles); }
    public boolean positionAvailable(String title) {
     
        for (Position position : this) 
            if(position.getTitle().equals(title) 
                    && position.getPerson() == Person.NULL)
                return true;
            return false;
    }
    public void fillPosition(String title,Person hire) {
     
        for (Position position : this) {
     
            if(position.getTitle().equals(title) 
                    && position.getPerson() == Person.NULL) {
     
                position.setPerson(hire);
                return;
            }
        }
        throw new RuntimeException("Position " + title + "not available");
    }
    public static void main(String[] args) {
     
        Staff staff = new Staff("President","CTO","Marketing Manager",
                        "Product Manager","Project Lead","Software Engineer",
                        "Software Engineer","Test Engineer","Technical Writer");
        staff.fillPosition("President", new Person("Me", "last", "The Top, Lonelt At"));
        staff.fillPosition("Project Lead", new Person("Janet", "Planner", "The Burbs"));
        if(staff.positionAvailable("Software Engineer")) 
            staff.fillPosition("Software Engineer", new Person("Bob", "Coder", "Bright Light City"));
    }
}

我们发现,在某些地方仍必须测试空对象,这与检查是否为null没有差异。但在其他地方,例如本例中的toString()中,则无需执行额外的测试了。

14.9 接口与类型信息

​ 接口的一个重要目标就是允许程序员隔离构件,进而降低耦合性。所以,在大多数情况下,我们会将实现类向上转型为接口,并返回给外界,从而实现隐藏细节。

​ 但是,我们经常在实现接口时,并非是与接口中的方法完全一致,时常会在实现类中增加额外的方法。但在这种情况下,客户端程序员可以通过使用RTTI,将接口进行转型,进而可以访问到这些额外的方法,使得他们的代码和类库存在耦合:

public interface A {
     
    void f();
}
 
class B implements A {
     
    public void f() {
     }
    public void g() {
     }
}
 
public class InterfaceViolation {
     
    public static void main(String[] args) {
     
        A a = new B();
        a.f();
        System.out.println(a.getClass().getSimpleName());
        if(a instanceof B) {
     
            B b = (B)a;
            b.g();
        }
    }
}

最简单的方式是对实现使用包访问权限:

class C implements A {
     
    public void f() {
      print("public C.f()"); }
    public void g() {
      print("public C.g()"); }
    void u() {
      print("package C.u()"); }
    protected void v() {
      print("protected C.v()"); }
    private void w() {
      print("private C.w()"); }
}
public class HiddenC {
     
    public static A makeA() {
      return new C(); }
}

这样,外界所能访问的唯一方法就是makeA():获取A类型对象。由于C类型在外界是无法访问到的,所以也无法将其进行向下转型:

public class HiddenImplementation {
     
    public static void main(String[] args) {
     
        A a = HiddenC.makeA();
        a.f();
        System.out.println(a.getClass().getName());
        // Compile error: cannot find symbol C
        // if( a instanceof C ) 
    }
}

不过,这也不是完全隐蔽的,通过反射,仍旧可以调用所有方法,甚至是private方法:

public class ReflectImplement {
     
    public static void main(String[] args) throws Exception {
     
        A a = HiddenC.makeA();
        callHiddenMethod(a, "g");
        callHiddenMethod(a, "u");
        callHiddenMethod(a, "v");
        callHiddenMethod(a, "w");
    }
    static void callHiddenMethod(Object obj,String methodName) throws Exception {
     
        Method method = obj.getClass().getDeclaredMethod(methodName);
        method.setAccessible(true);
        method.invoke(obj);
    }
}

如果将接口实现为一个私有内部类呢:

class InnerA {
     
    private static class C implements A {
     
        public void f() {
      print("public C.f()"); }
        public void g() {
      print("public C.g()"); }
        void u() {
      print("package C.u()"); }
        protected void v() {
      print("protected C.v()"); }
        private void w() {
      print("private C.w()"); }
    }
    public static A makeA() {
      return new C(); }
}
 
public class InnerImplementation {
     
    public static void main(String[] args) throws Exception {
     
        A a = InnerA.makeA();
        a.f();
        System.out.println(a.getClass().getName());
        ReflectImplement.callHiddenMethod(a, "g");
        ReflectImplement.callHiddenMethod(a, "u");
        ReflectImplement.callHiddenMethod(a, "v");
        ReflectImplement.callHiddenMethod(a, "w");
    }
}

结果发现仍旧无法对反射进行隐藏,那么如果是匿名类呢:

class AnonymousA {
     
    public static A makeA() {
     
        return new A() {
     
            public void f() {
      print("public C.f()"); }
            public void g() {
      print("public C.g()"); }
            void u() {
      print("package C.u()"); }
            protected void v() {
      print("protected C.v()"); }
            private void w() {
      print("private C.w()"); }
        };
    }
}
public class AnonymousImplementation {
     
    public static void main(String[] args) throws Exception {
     
        A a = AnonymousA.makeA();
        a.f();
        System.out.println(a.getClass().getName());
        ReflectImplement.callHiddenMethod(a, "g");
        ReflectImplement.callHiddenMethod(a, "u");
        ReflectImplement.callHiddenMethod(a, "v");
        ReflectImplement.callHiddenMethod(a, "w");
    }
}

目前尝试下来,没有任何方式可以阻止反射调用那些非公共访问权限的方法。下面将尝试反射对于域的访问和修改情况:

class WithPrivateFinalField {
     
    private int i = 1;
    private final String s = "I'm totally safe";
    private String s2 = "Am I safe?";
    public String toString() {
     
        return "i = " + i + ", " + s + ", " + s2;
    }
}
 
public class ModifyingPrivateFields {
     
    public static void main(String[] args) throws Exception {
     
        WithPrivateFinalField pf = new WithPrivateFinalField();
        System.out.println(pf);
        
        Field f = pf.getClass().getDeclaredField("i");
        f.setAccessible(true);
        System.out.println("f.getInt(pf): " + f.getInt(pf) );
        f.setInt(pf, 66);
        System.out.println(pf);
        
        f = pf.getClass().getDeclaredField("s");
        f.setAccessible(true);
        System.out.println("f.get(pf): " + f.get(pf) );
        f.set(pf, "No, you're not!");
        System.out.println(pf);
        
        f = pf.getClass().getDeclaredField("s2");
        f.setAccessible(true);
        System.out.println("f.get(pf): " + f.get(pf) );
        f.set(pf, "No, you're not!");
        System.out.println(pf);
    }
}

可以发现,当反射作用于域时,仍然可以访问并修改非公共访问权限的域。注意,在final域遭遇反射修改时,运行系统会在不抛异常的情况下接受任何修改,但实际上并没有做出修改。

因此,反射机制是一把双刃剑。它违反了访问权限,但也能解决一些疑难问题。

14.10 Java反射机制总结

​ RTTI允许通过匿名基类的引用来发现类型信息。但在面向对象程序语言中:凡是可以使用的地方都使用多态机制,只在必需时使用RTTI。

​ 反射允许更加动态的编程风格,其开创了编程的新世界。

# 反射的好处:
		1. 可以在程序运行过程中,操作这些对象。
		2. 可以解耦,提高程序的可扩展性。

# 获取Class对象的方式:
        1. Class.forName("全类名"):将字节码文件加载进内存,返回Class对象
            * 多用于配置文件,将类名定义在配置文件中。读取文件,加载类
        2. 类名.class:通过类名的属性class获取
            * 多用于参数的传递
        3. 对象.getClass():getClass()方法在Object类中定义着。
            * 多用于对象的获取字节码的方式

# 结论:
		同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。
		
# Class对象功能:
# 1.获取成员变量们
			* Field[] getFields() :获取所有public修饰的成员变量
			* Field getField(String name)   获取指定名称的 public修饰的成员变量

			* Field[] getDeclaredFields()  获取所有的成员变量,不考虑修饰符
			* Field getDeclaredField(String name)  
			
# 2.获取构造方法们
			* Constructor[] getConstructors()  
			* Constructor getConstructor(类... parameterTypes)  

			* Constructor getDeclaredConstructor(类... parameterTypes)  
			* Constructor[] getDeclaredConstructors() 
            
# 3.获取成员方法们:
			* Method[] getMethods()  
			* Method getMethod(String name, 类... parameterTypes)  

			* Method[] getDeclaredMethods()  
			* Method getDeclaredMethod(String name, 类... parameterTypes)  

# 4.获取全类名	
			* String getName() 
			
# Field:成员变量
# 操作:
		1. 设置值
			* void set(Object obj, Object value)  
		2. 获取值
			* get(Object obj) 

		3. 忽略访问权限修饰符的安全检查
			* setAccessible(true):暴力反射
			
# Constructor:构造方法
	* 创建对象:
		* T newInstance(Object... initargs)  

		* 如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法
		
# Method:方法对象
	* 执行方法:
		* Object invoke(Object obj, Object... args)  

	* 获取方法名称:
		* String getName:获取方法名

第15章 泛型

泛型Java编程思想总结

  • 一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大
  • 泛型实现了参数化类型的概念,使代码可以应用于多种类型

15.1 与C++的比较

  1. 了解C++模板的某些方面,有助于你理解泛型的基础
  2. 可以了解Java泛型的局限是什么,以及为什么会有这些限制。理解Java泛型的边界在哪里

15.2 简单泛型

  1. 有许多原因促进了泛型的出现,而最引人注目的一个原因,就是为了创造容器类

  2. 使用类型参数,用尖括号括住,放在类名后面

    public class Holder3<T> {
           
        private T a;
        public Holder3(T a){
           
            this.a = a;
        }
        public void set(T a){
           
            this.a = a;
        }
        public T get(){
           
            return a;
        }
        public static void main(String[] args){
           
            Holder3<Automobile> h3 = new Holder3<Automobile>(new Automobile());
            Automobile a = h3.get();
        }
    }
    
  3. Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节

  4. 一个元组类库
    a. 元组:将一组对象直接打包存储于其中的一个单一对象。这个容器对象允许读取其中元素,但是不允许向其中存放新的对象。(这个概念也称为数据传递对象,或信使

    public class TowTuple<A,B>{
           
        public final A first;
        public final B second;
        public TowTuple(A a, B b){
           
            first = a;
            second = b;
        }
        public String toString(){
           
            return "(" +first + ", " +second + ")";
        }
    }
    

    b. 我们可以利用继承机制实现长度更长的元组

    public class ThreeTuple<A,B,C> extends TwoTuple<A,B>{
           
        public final C third;
        public ThreeTuple(A a, B b, C c){
           
            super(a, b);
            third = c;
        }
        public String toString(){
           
            return "(" +first + ", " +second + ", " + third +")";
        }
    }
    
    
  5. 一个堆栈类
    a. 可以在使用泛型的类中继续使用泛型

    public class LinkedStack<T>{
           
        private static class Node<U>{
           
            
        }
    }
    
  6. RandomList
    a. 调用select()可以实现在列表中随机选择一个元素

15.3 泛型接口

  1. 泛型也可以应用于接口。例如生成器,这是一种专门负责创建对象的类。生成器无需额外的信息就知道如何创建新对象

    public interface Generator<T> {
            T next();}
    
  2. Java泛型的一个局限性:基本类型无法作为类型参数。但Java实现了很好的自动打包和自动拆包的功能

15.4 泛型方法

  1. 是否拥有泛型方法,与其所在的类是否是泛型没有关系

  2. 无论何时,只要你能做到,你就应该尽量使用泛型方法

  3. 对于一个static的方法而言,无法访问泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法

    public class GenericMethods{
           
        public <T> void f(T x){
           
            System.out.println(x.getClass().getName());
        }
        public static void main(String[] args){
           
            GenericMethods gm = new GenericMethods();
            gm.f("");
            gm.f(1);
            gm.f(1.0);
            gm.f(1.0F);
            gm.f('c');
            gm.f(gm);
        }
    }
    
  4. 杠杆利用类型参数推断
    a. 类型参数推断避免了重复的泛型参数列表
    b. 类型推断只对赋值操作有效,其他时候并不起作用
    c. 显示的类型说明,很少使用

    public calss ExplicitTypeSpecification{
           
        static void f(Map<Person, List<Pet>> petPeople){
           }
        public static void main(String[] args){
           
            f(New., List<Pet>>map());
        }
    }
    
  5. 可变参数与泛型方法
    a. 泛型方法与可变参数列表能够很好的共存

    public class GenericVarargs{
           
        public static <T> List<T> makeList(T... args){
           
            List<T> result = new ArrayList<T>();
            for(T item: args){
           
                result.add(item);
            }
            return result;
        }
    }
    
  6. 用于Generator的泛型方法
    a. 利用生成器,我们可以很方便地填充一个Collection,而泛型化这种操作是具有实际意义

    public class Generators{
           
        public static <T> Collection<T> fill(Collection<T> coll, Generator<T> gen, int n){
           
            for(int i=0; i < n; i++){
           
                coll.add(gen.next());
            }
            return coll;
        }
    }
    
  7. 一个通用的Generator
    a. 这个类必须声明为public
    b. 它必须具备默认的构造器(无参数的构造器)

  8. 简化元组的使用

  9. 一个Set使用工具

15.5 匿名内部类

  1. 泛型还可以应用于内部类以及匿名内部类
public Teller{
     
    private static long counter = 1;
    private final long id = counter++;
    private Teller(){
     }
    public String toString(){
     return "Teller " +id;}
    public static Generator<Teller> generator = 
        new Generator<Teller>(){
     
            public Teller next(){
     return new Teller();}
        };
}

15.6 构建复杂模型

  1. 泛型的一个重要好处是能够既简单而安全地创建复杂的模型
  2. 可以组装多层容器,构建更加复杂的数据结构

15.7 擦除的神秘之处

  • 编译器会认为**ArrayListArrayList**是同一类型
  • Class.getTypeParameters()将返回一个TypeVariable对象数组,表示有泛型声明的类型参数。但是只能返回用作参数占位符的标识符,对类型判断没啥意义
  • 在泛型代码内部,无法获取任何有关泛型参数类型的信息
  • Java泛型是使用擦除来实现的,这意味着你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象

C++的方式
a. 当实例化模板时,C++编译器将进行检查,能够得知参数的类型。如果情况并非如此,就得到一个编译期错误,这样类型安全就得到了保障

b. Java中,边界****声明T必须具有类型HasF或者HasF导出的类型
c. 必须查看所有的代码,并确定它是否“足够复杂”到必须使用泛型的程度

迁移兼容性
a. 泛型在Java中仍旧是有用的,只是不如他们本来设想的那么有用,而原因就是擦除
b. 在基于擦除的实现中,泛型类型被当做第二类类型处理,即不能再某些重要的上下文环境中使用的类型。泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为他们的非泛型上界
c. 擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来实现,反之亦然,这经常被称为“迁移兼容性

d. 通过允许非泛型代码与泛型代码共存,擦除使得这种向着泛型的迁移成为可能

擦除的问题
a. 擦除的主要正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下,即将泛型融入Java语言
b. 为了关闭警告,Java提供了一个注解。注意,这个注解被放置在可以产生这类警告的方法之上,而不是整个类上

@SuppressWarnings("unchecked");

边界处的动作
a. 对于在泛型中创建数组,使用Array.newInstance()是推荐的方式
b. 即使操出在方法或类内部移除了有关实际类型的信息,编译器仍然可以确保在方法或类中使用的类型的内部一致性

c. 泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转型。“边界就是发生动作的地方”

15.8 擦除的补偿

  • 擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道确切类型信息的操作都将无法工作
  • 通过引入类型标签来对擦除进行补偿。编译器即将确保类型标签可以匹配泛型参数
class Building{
     }
class House extends Building{
     }

public class ClassTypeCapture<T> {
     
    Class<T> kind;
    public ClassTypeCapture(Class<T> kind){
     
        this.kind = kind;
    }
    public boolean f(Object arg){
     
        return kind.isInstance(arg);
    }
    public static void main(String[] args){
     
        ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);
        System.out.println(ctt1.f(new Building()));
        System.out.println(ctt1.f(new House()));
        ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);
        System.out.println(ctt2.f(new Building()));
        System.out.println(ctt2.f(new House()));
    }
}
  1. 创建类型实例
    a. 建议使用显式的工厂,并将闲置其类型,使得只能接受实现了这个工厂的类
    b. 利用模板方法设计模式
  2. 泛型数组
    a. 不能创建泛型数组。一般的解决方案是在任何想要创建泛型数组的地方都使用ArrayList
public class ListOfGenerics<T>{
     
    private List<T> array = new ArrayList<T>();
    public void add(T item){
      array.add(item);}
    public T get(int index){
      return array.get(index);}
}

b. 成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型
c. 有了擦除,数组的运行时类型就只能是Object[]
d. 最好是在集合内部使用Object[],然后当你使用数组元素时,添加一个对T的转型
e. 警告的特殊效果:当你发现可以忽略他们时,你就可以忽略
f. 即使在Java类库源代码中出现了某些惯用法,也不能表示这就是正确的解决之道。当查看类库代码时,你不能认为它就是应该在自己的代码中遵循的示例

15.9 边界

  1. 边界使得你可以在用于泛型的参数类型上设置限制条件
  2. 可以用无界泛型参数调用的方法只是那些可以用Object调用的方法。但是,如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法

15.10 通配符

  • 数组的一种特殊行为:可以向导出类型的数组赋予基类型的数组引用
  • 泛型不仅和容器相关正确的说法是:“不能把一个涉及Apple的泛型赋值给一个涉及Fruit的泛型“。AppleList在类型上不等价与FruitList
  • 在两个类型之间建立某种类型的向上转型关系,这正是通配符所允许的
    a. flist类型现在是List,可以读作“具有任何从Fruit继承的类型的列表
List<? extends Fruit> flist = new ArrayList<Apple>();
  1. 编译器有多聪明
    a. 编译器将直接拒绝对参数列表中涉及通配符的方法的调用

  2. 逆变

    a. 超类型通配符:这里,可以声明通配符是由某个特定类的任何基类来界定的,方法时指定**,甚至或者使用类型参数:**。这使得你完全可以安全地传递一个参数对象到泛型类型中

    public class SuperTypeWildcards{
           
        static void writeTo(List<? super Apple> apples){
           
            apples.add(new Apple());
            apples.add(new Jonathan());
        }
    }
    

    b. 超类型边界放松了在可以向方法传递的参数上的所作限制

  3. 无界通配符
    a. **无界通配符**看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型
    b. 无界通配符在声明:“我是想用Java的泛型来编写这段代码,我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型

    c. 当你在处理多个泛型参数时,有时允许一个参数可以是任何类型的,同时为其他参数确定某种特定类型的这种能力会显得很重要
    d. List实际表示“持有任何Object类型的原生List”,而**List**表示“具有某种特定类型的非原生List,只是我们不知道那种类型是什么

    e. 无论何时,只要使用了原生类型,就会放弃编译期检查
    f. 原生Holder将持有任何类型的组合,而Holder将持有具有某种具体类型的同构集合,因此不能只是向其中传递Object

  4. 捕获转换
    如果向一个使用****的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法

15.11 问题

  1. 任何基本类型都不能作为类型参数
    a. 但Java中的自动包装机制可以解决,甚至可以使用foreach方法

  2. 实现参数化接口
    a. 一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口

  3. 转型和警告
    a. 使用带有泛型类型参数的转型或instanceof不会有任何效果
    b. 通过泛型类来转型:

    List<Widget> lw2 = List.class.cast(in.readObject());
    
  4. 重载
    由于擦除的原因,重载方法将产生相同的类型签名,即不可出现如下情况

    public class UseList<W,T>{
           
        void f(List<T> v){
           }
        void f(List<W> v){
           } //由于擦除的存在,是同一种类型
    }
    
  5. 基类劫持了接口
    a. 一旦为Comparable确定了ComparablePet参数,那么其他任何实现类都不能与ComparablePet之外的任何对象比较

15.12 自限定的类型

class SelfBound<T extends SelfBounded<T>>{
     }
  1. 古怪的循环泛型
    a. 不能直接继承一个泛型参数,但是,可以继承在其自己的定义中使用这个泛型参数的类
    b. “古怪的循环”是指相当古怪地出现在自己的基类中这一事实
    c. Java中的泛型关乎参数和返回类型,因此它能够产生使用导出类作为其参数和返回类型的基类

    public class BasicHolder<T>{
           
        T element;
        void set(T arg){
           element = arg;}
        T get(){
            return element;}
        void f(){
           
            System.out.println(element.getClass().getSimpleName());
        }
    }
    
    class Subtype extends BasicHolder<Subtype> {
           }
    public class CRGWithBasicHolder{
           
        public static void main(String[] args){
           
            Subtype st1 = new Subtype(), st2 = new Subtype();
            st1.set(st2);
            Subtype st3 = st1.get();
            st1.f();
        }
    }
    

    d. 新类Subtype接受的参数和返回的值具有Subtype类型额而不仅仅是基类BasicHolder类型
       e. CRG的本质:基类用导出类替代其参数

  2. 自限定
    a. 自限定将采取额外的步骤,强制泛型当作其自己的边界参数来使用
    b. 自限定所做的,就是要求在继承关系中,像下面这样使用这个类。这会强制要求正在定义的类当作参数传递给基类
    c. 自限定限制只能强制作用于继承关系

    class A extends SelfBounded<A>{
           }
    
  3. 参数协变
    a. 自限定类型的价值在于它们可以产生协变参数类型——方法参数类型会随子类而变化
    b. 自限定泛型事实上将产生确切的导出类型作为其返回值

15.13 动态类型安全

a. java.util.Collections中有一组便利工具,可以解决在这种情况下的类型检查问题,它们是:静态方法checkedCollection()checkedList()、checkedMap()、checkedSet()、checkedSortedMap()和checkedSortedSet()。这些方法每一个都会将你希望动态检查的容器当做第一个参数接受,并将你希望强制要求的类型作为第二个参数接受
b. 受检查的容器在你试图插入类型不正确的对象时抛出ClassCastException

15.14 异常

  1. 由于擦除的原因,将泛型用于异常时非常受限的
  2. catch语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型
  3. 泛型也不能直接或间接继承自Throwable
  4. 类型参数可能会在一个方法的throws子句中用到。这使得可以编写随检查型异常的类型而发生变化的泛型代码

15.15 混型

  • 概念:混合多个类的能力,以产生一个可以表示混型中所有类型的类
    1. C++中的混型
      a. 对于混型来说,更有趣、更优雅的方式是使用参数化类型,因为混型就是继承自其类型参数的类
      b. 在C++中,可以很容易的创建混型,因为C++能够记住其模板参数的类型
    2. 与接口混合
      a. 一种更常见的推荐解决方案是使用接口来产生混型效果
    3. 使用装饰器模式
      a. 装饰器经常用于满足各种可能的组合,而直接子类化会产生过多的类,因此是不实际的
      b. 装饰器模式使用分层对象来动态透明地向单个对象中添加责任
      c. 装饰器只是对由混型提出的问题的一种局限的解决方案
    4. 与动态代理混合
      a. 通过使用动态代理,所产生的类的动态类型将会是已经混入的组合类型

15.16 潜在类型机制

  1. 如果代码不关心它将要作用的类型,那么这种代码就可以真正地应用于任何地方,并因此而相当“泛化”
  2. 泛型代码典型地将在泛型类型上调用少量方法,而具有潜在类型机制的语言只要求实现某个方法子集,而不是某个特定类或接口,从而放松了这种限制(并且可以产生更加泛化的代码)
  3. 潜在类型机制是一种代码组织和复用机制
  4. 两种支持潜在类型机制的语言是Python和C++
  5. 潜在类型机制没有损害强类型机制

15.17 对缺乏潜在类型机制的补偿

  1. 反射
    a. 可以使用的一种方式是反射
    b. 通过反射,静态方法能够动态的确定所需要的方法是否可用并调用它们
  2. 将一个方法应用于序列
    a. 类型标记技术是Java文献推荐的的技术
  3. 当你并未碰巧拥有正确的接口时
  4. 用适配器仿真潜在类型机制
    a. Java泛型并不是没有潜在类型机制,而我们需要像潜在类型机制这样的东西去编写能够跨类边界应用的代码
    b. 从我们拥有的接口中编写代码来产生我们需要的接口,这是适配器设计模式的一个典型示例

15.18 将函数对象用作策略

  1. 解决方案是使用策略设计模式,这种设计模式可以产生更优雅的代码,因为它将“变化的事物”完全隔离到了一个函数对象

15.19 总结:转型真的如此之糟吗?

  1. 使用泛型类型机制的最吸引人的地方,就是在使用容器类的地方,这些类包括诸如各种List、各种Set、各种Map
  2. 泛型,它是一种方法,通过它可以编写更“泛化”的代码,这些代码对于它们能够作用的类型具有更少的限制,因此单个的代码段可以应用到更多的类型上

泛型 菜鸟教程总结

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?

答案是可以使用 Java 泛型

使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。

泛型方法

你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

下面是定义泛型方法的规则:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的)。
  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)。

实例

下面的例子演示了如何使用泛型方法打印不同字符串的元素:

public class GenericMethodTest
{
     
   // 泛型方法 printArray                         
   public static < E > void printArray( E[] inputArray )
   {
     
      // 输出数组元素            
         for ( E element : inputArray ){
             
            System.out.printf( "%s ", element );
         }
         System.out.println();
    }
 
    public static void main( String args[] )
    {
     
        // 创建不同类型数组: Integer, Double 和 Character
        Integer[] intArray = {
      1, 2, 3, 4, 5 };
        Double[] doubleArray = {
      1.1, 2.2, 3.3, 4.4 };
        Character[] charArray = {
      'H', 'E', 'L', 'L', 'O' };
 
        System.out.println( "整型数组元素为:" );
        printArray( intArray  ); // 传递一个整型数组
 
        System.out.println( "\n双精度型数组元素为:" );
        printArray( doubleArray ); // 传递一个双精度型数组
 
        System.out.println( "\n字符型数组元素为:" );
        printArray( charArray ); // 传递一个字符型数组
    } 
}

编译以上代码,运行结果如下所示:

整型数组元素为:
1 2 3 4 5 

双精度型数组元素为:
1.1 2.2 3.3 4.4 

字符型数组元素为:
H E L L O 

有界的类型参数:

可能有时候,你会想限制那些被允许传递到一个类型参数的类型种类范围。例如,一个操作数字的方法可能只希望接受Number或者Number子类的实例。这就是有界类型参数的目的。

要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界。

实例

下面的例子演示了"extends"如何使用在一般意义上的意思"extends"(类)或者"implements"(接口)。该例子中的泛型方法返回三个可比较对象的最大值。

public class MaximumTest
{
     
   // 比较三个值并返回最大值
   public static <T extends Comparable<T>> T maximum(T x, T y, T z)
   {
                          
      T max = x; // 假设x是初始最大值
      if ( y.compareTo( max ) > 0 ){
     
         max = y; //y 更大
      }
      if ( z.compareTo( max ) > 0 ){
     
         max = z; // 现在 z 更大           
      }
      return max; // 返回最大对象
   }
   public static void main( String args[] )
   {
     
      System.out.printf( "%d, %d 和 %d 中最大的数为 %d\n\n",
                   3, 4, 5, maximum( 3, 4, 5 ) );
 
      System.out.printf( "%.1f, %.1f 和 %.1f 中最大的数为 %.1f\n\n",
                   6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 ) );
 
      System.out.printf( "%s, %s 和 %s 中最大的数为 %s\n","pear",
         "apple", "orange", maximum( "pear", "apple", "orange" ) );
   }
}

编译以上代码,运行结果如下所示:

3, 4 和 5 中最大的数为 5

6.6, 8.8 和 7.7 中最大的数为 8.8

pear, apple 和 orange 中最大的数为 pear

泛型类

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。

和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

实例

如下实例演示了我们如何定义一个泛型类:

public class Box<T> {
     
   
  private T t;
 
  public void add(T t) {
     
    this.t = t;
  }
 
  public T get() {
     
    return t;
  }
 
  public static void main(String[] args) {
     
    Box<Integer> integerBox = new Box<Integer>();
    Box<String> stringBox = new Box<String>();
 
    integerBox.add(new Integer(10));
    stringBox.add(new String("菜鸟教程"));
 
    System.out

你可能感兴趣的:(Java基础,java)