类和对象

来源:互联网 发布:淘宝销量可以刷吗 编辑:程序博客网 时间:2024/06/06 18:44

  • 类和对象
      • 声明类Declaring Classes
      • 声明成员变量declaring member variables
      • 为你的类提供构造函数
      • 向构造函数或者方法传递信息
    • 对象
      • 创建对象
      • 使用对象
    • 更多关于类的内容more on class
      • 从方法中返回值
      • 类成员的访问控制权限Controlling Access to members of a Class
      • 理解类成员Understanding Class Members
      • 初始化字段Initializing Fields
      • 理解类成员Understanding Class Members
      • 初始化字段Initializing Fields
    • 嵌套类Nested Class
      • 内部类例子Inner Class Example
      • 局部内部类Local Classes
      • 匿名内部类Anonymous Classes
    • Lambda 表达式Lambda Expressions
      • 诠释Lambda的完美例子
        • 方法一创建一个满足一个特征的成员
        • 方法二创建更加普遍Generalized的搜索方法
        • 方法三在内部类中指定具体化查询条件
        • 方法四用匿名内部类来实现标准查询
        • 方法五用Lambda表达式来指定查询条件
        • 方法六Lambda表达式与标准函数接口standard Functional方法结合使用
        • 方法七在你的应用程序中使用Lambda表达式
        • 方法八用泛型使更加通用化
        • 方法九使用接受Lambda表达式作为参数的聚合操作
        • Lambda表达式在GUI中的使用
        • Lambda表达式的语法
      • 方法引用method references
    • 何时使用嵌套类局部内部类匿名内部类以及Lambda表达式
    • 枚举类型Enum Types

类和对象

掌握了JAVA编程语言基础知识后,你便可以编写你自己的类。在本章节中,你将学到如何定义类、声明成员变量、方法和构造函数。此外你还可以学习如何使用定义的类来创建对象,以及使用对象。

本节课也会涵盖嵌套类以及枚举类。

在面向对象编程概念中用自行车(Bicycle)的例子来介绍面向对象的概念。下面是Bicycle类的样例,以说明类声明的概览。这节课的系列章节将一步步为你解释类的声明。现在不用太关注细节。

public class Bicycle {    // the Bicycle class has    // three fields    public int cadence;    public int gear;    public int speed;    // the Bicycle class has    // one constructor    public Bicycle(int startCadence, int startSpeed, int startGear) {        gear = startGear;        cadence = startCadence;        speed = startSpeed;    }    // the Bicycle class has    // four methods    public void setCadence(int newValue) {        cadence = newValue;    }    public void setGear(int newValue) {        gear = newValue;    }    public void applyBrake(int decrement) {        speed -= decrement;    }    public void speedUp(int increment) {        speed += increment;    }}

Bicycle的子类MountainBicycle的声明如下:

public class MountainBike extends Bicycle {    // the MountainBike subclass has    // one field    public int seatHeight;    // the MountainBike subclass has    // one constructor    public MountainBike(int startHeight, int startCadence,                        int startSpeed, int startGear) {        super(startCadence, startSpeed, startGear);        seatHeight = startHeight;    }       // the MountainBike subclass has    // one method    public void setHeight(int newValue) {        seatHeight = newValue;    }   }

MountainBicycle继承Bicycle所有的字段和方法,并添加了额外的字段seatHeight和一个设置seatHeight值的方法(山地车拥有可根据地形的改变而上下移动的座位);

声明类(Declaring Classes)

你已经看到类的定义的方式如下:

class MyClass {    // field, constructor, and     // method declarations}

这就是类的声明。类的body(即{}内)包含对象(类实例化的产物)的生命周期代码:构造函数用于初始化新的对象;声明的字段用于表示类和它的对象的状态(state);方法用于实现类和它的对象的行为(behavior).

前面例子中,类的声明是最小的。它仅仅包含类声明所需的组件。你可以在类声明的起始点为类提供更多的信息,比如子类的子类,它是否实现了接口等等。例如:

class MyClass extends MySuperClass implements YourInterface {    // field, constructor, and    // method declarations}

表示MyClass是MySuperClass的子类,它实现类YourInterface接口。

开始的时候你也可以添加修饰词如public、private——因此你可以看到类的声明也可以变得复杂。可以决定其他类是否可以访问MyClass的修饰词public和private将会在后面介绍。

通常,类的声明可以包含以下组件:

  • 修饰词,如public、private等。
  • 类的名称。按照约定,类的名称首字母要大写。
  • 父类的名称。类只能继承自一个父类,继承的关键字是extends。
  • 要实现接口的列表,用逗号分隔开。一个类可以实现多个接口,实现的关键字是implements。
  • 类的主体(body),用{}包裹。

声明成员变量(declaring member variables)

这里有三种类型的变量:

  1. 类内的成员变量 —— 称为字段(fields);
  2. 块或方法内的变量 —— 称为局部变量(local variables);
  3. 方法声明中的变量 —— 称为参数。

Bicycle类中使用以下的代码来声明它的字段:

public int cadence;public int gear;public int speed;

字段声明由3部分构成,它们依次是:
1. 0个或者多个修饰词,如public、private等。
2. 字段的类型。
3. 字段的名称。

Bicycle的字段名称有cadence,gear和speed,并且它们都是int类型。关键字public定义了这些字段为共有(public)成员,意味着其他任意对象可以访问到。

访问权限修饰词(access modifiers)

访问权限修饰词允许你控制其它类是否能够访问你的成员字段。现在只考虑public和private,其它的之后在讨论。

  • public修饰词——字段可以被任意类所访问。
  • private修饰词——字段仅能被类内使用。

遵循封装的规则,通常会把字段声明为private。意味着,它们只能被Bicycle类所访问。我们还是得需要访问这些值,然而,这是可以通过直接添加获取这些值的public方法的,如:

public class Bicycle {    private int cadence;    private int gear;    private int speed;    public Bicycle(int startCadence, int startSpeed, int startGear) {        gear = startGear;        cadence = startCadence;        speed = startSpeed;    }    public int getCadence() {        return cadence;    }    public void setCadence(int newValue) {        cadence = newValue;    }    public int getGear() {        return gear;    }    public void setGear(int newValue) {        gear = newValue;    }    public int getSpeed() {        return speed;    }    public void applyBrake(int decrement) {        speed -= decrement;    }    public void speedUp(int increment) {        speed += increment;    }}

类型

所有的变量必须有一个类型。你可以声明它为原始数据类型,如int,float和boolean等等。或者声明它们为引用类型,如strings,arrays,和objects.

变量名称

所有的变量,无论它们是字段,局部变量还是参数都遵循着一样的规则和约定。

在本节课,应该意识到所有的命名规则和约定同样被运用到方法和类名上,除了:
1. 类名首字母要大写。
2. 方法名的第一个单词应该是动词。

定义方法

下面是一个声明方法的典型例子:

public double calculateAnswer(double wingSpan, int numberOfEngines,                              double length, double grossTons) {    //do the calculation here}

声明方法的必要元素有方法的返回类型、名称、参数、()、和被{}包围的方法体。

通常情况下,方法声明由6个组成部分,顺序表示如下:
1. 修饰词——如public、private等。
2. 返回类型——方法返回值的数据类型,或者不返回类型的话用void表示。
3. 方法名称——方法名称的命名规则和约定和字段的一样,只是稍微有点不同。
4. 括号中的参数列表——逗号分隔的参数列表。如果没有参数,则必须用空括号。
5. 异常列表——稍后讨论。
6. 方法体,被{}包围。

定义:方法声明的两个组件组成方法的签名——方法名称和参数类型。

方法签名(method signatures)声明如下:

calculateAnswer(double, int, double, double)

命名一个方法(naming a method)

尽管方法名称可以是任意合法的标识符,但是代码约定约束着方法的名称。按照约定,方法名应该是小写的一个动词,或者首个单词是小写且为动词,接下来是连接词、名词等。由多单词构成的方法名称,除第一个单词之外,每个单词的首字母应该大写,例如:

run runFastgetBackgroundgetFinalDatacompareTosetXisEmpty

典型的,一个类内的方法名称应该是唯一的,但由于允许方法重载(method overloading),类内方法名称可以重名。

重载方法(overloading methods)

java编程语言支持重载方法,Java可以通过方法签名(method signatures)来区分它们。这意味着同一个类内的方法在参数列表不同(参数个数、参数类型不同)的情况下,方法可以同名。

假设你有一个可以绘画不同的数据类型(如int、String等).用不同名称为每个方法命名的方式很麻烦——例如drawString,drawInteger,drawFloat等等。在Java编程语言中,你可以为所有的绘画方法取相同的名称,但是需要为每个方法传递不同的参数。因此,绘画数据类可能声明了4个draw方法,每个方法拥有不一样的参数列表:

public class DataArtist {    ...    public void draw(String s) {        ...    }    public void draw(int i) {        ...    }    public void draw(double f) {        ...    }    public void draw(int i, double f) {        ...    }}

重载方法通过传递给方法的参数类型和参数个数加以区分。上述代码例子中draw(String s)draw(int i)是通过参数类型不同加以区分的。

你不能声明方法名称相同,参数类型,参数个数相同的方法,因为编译器不能对它们进行区分。

在区分方法时,编译器不会对方法返回类型进行考虑,因此你不能声明方法签名(方法名称相同、参数列表一样)一样,返回类型不一样的方法。

方法重载应该少用(sparingly:节约、节制地),因为它们会让代码难以阅读。

为你的类提供构造函数

以类蓝图(class blueprint)为模板,通过调用类的构造函数创建出对象。构造函数的声明与方法的声明相似——除了它没有返回值,已经名称和类名一样之外。例如,Bicycle有一个构造函数:

public Bicycle(int startCadence, int startSpeed, int startGear) {    gear = startGear;    cadence = startCadence;    speed = startSpeed;}

为了创建一个叫myBikeBicycle对象,通过new操作调用构造函数:

Bicycle myBike = new Bicycle(30, 0, 8);

new Bicycle(30,0,8)会在内存中开辟一个空间,并初始化它的字段。

尽管Bicycle只有一个构造函数,但是它可以拥有其他的构造函数,包括无参构造函数:

public Bicycle() {    gear = 1;    cadence = 10;    speed = 0;}

Bicycle yourBike = new Bicycle();调用无参构造函数创建一个新的,名称为myBikeBicycle对象。

两个构造函数都可以在Bicycle内声明,因为他们有不同的参数列表。对于方法而言,Java平台通过参数的个数和参数的方法来区分构造函数。你不能编写两个参数个数相同,参数类型相同的构造函数,因为平台无法区分它们,如果硬要这样编写,则会产生编译错误(compile-error)。

你可以不为你的类编写任何的构造函数,但是须得小心。编译器会自动地为那些没有构造函数的类创建一个默认的无参构造函数。默认的构造函数会自发地调用父类的无参(no-argument)构造函数。这种情况下,假如父类灭有默认的无参构造函数,编译器会抱怨(there is no default constructor available in superClassName)。假如类没有父类,隐形地,它的父类是java.lang.Object,Object类是有无参构造函数的。

你可以在声明构造函数的时候加上修饰词(modifiers),这样就可以控制其它类调用该构造函数了。

假如其它类不能调用MyClass构造函数,那么它不能直接创建MyClass对象。

向构造函数或者方法传递信息

声明函数或者构造函数的时候会指定参数的个数和类型。例如,下面是一个计算住房贷款月的方法,

public double computePayment(                  double loanAmt,                  double rate,                  double futureValue,                  int numPeriods) {    double interest = rate / 100.0;    double partial1 = Math.pow((1 + interest),                     - numPeriods);    double denominator = (1 - partial1) / interest;    double answer = (-loanAmt / denominator)                    - ((futureValue * partial1) / denominator);    return answer;}

方法拥有4个参数:贷款金额(loan amount)、利率(interest rate)、future value 和 numbers of periods。参数在方法体中使用,在运行期间传入的值会赋给这些参数。

参数指的是方法声明中的变量列表。参数的值是当方法被调用时传入的值。当你调用一个方法,传入数据的类型和顺序应以方法声明的参数类型和顺序相匹配。

参数类型

函数或构造函数的参数可以是任意的数据类型。包括原始数据类型,如doubles,floats等;以及引用数据类型,如objects和arrays。

下面是一个用数组作为参数的方法。在这个例子中,方法创建一个新的Polygon对象,并用一个类型为Point的数组初始化:

public Polygon polygonFrom(Point[] corners) {    // method body goes here}

假如你想让方法作为方法的参数,可以使用lambda或方法引用实现。


任意数量(Arbitrary Number)的参数

你可以使用可变参数(varargs),向一个方法传递任意数量的参数值。当你不知道方法的参数有多少个时,你会用到可变参数。这是一种手动创建数组的快捷方式(前面代码例子中,可以用可变参数替代)。

为了使用可变参数,紧接着参数类型的是省略号(三个点),随后是空格,然后才是参数名称。方法调用时,可以传递任意数量的参数,包括不传参数:

public Polygon polygonFrom(Point... corners) {    int numberOfSides = corners.length;    double squareOfSide1, lengthOfSide1;    squareOfSide1 = (corners[1].x - corners[0].x)                     * (corners[1].x - corners[0].x)                      + (corners[1].y - corners[0].y)                     * (corners[1].y - corners[0].y);    lengthOfSide1 = Math.sqrt(squareOfSide1);    // more method body code follows that creates and returns a     // polygon connecting the Points}

从代码中你可以看到,corners被当成了数组。调用方法是可以传递一个数组,或者一个参数的序列。调用时,方法体会将参数看成数组。

通常你会在打印方法中看到可变参数,例如:

System.out.printf("%s: %d, %s%n", name, idnum, address);System.out.printf("%s: %d, %s, %s, %s%n", name, idnum, address, phone, email);

参数名称

当你为函数或者构造函数声明一个参数的时候,你为这个参数提供了一个名称。这个参数名称只在方法体内部使用,它指向的是调用时传递进来值(如果是原始数据类型,那参数的值就是传递(passed-in)进来的值)。

参数名称在这个范围内必须是唯一的。函数或构造函数的参数如果有多个的话,参数的名称不能相同;参数名称也不能和函数或构造函数方法体内的局部变量名称相同。

参数名称可以和类的字段(fields)的名称一样,此时参数会遮蔽(shadow)同名字段,这会使得代码难以阅读。按照约定,只有在设置类字段的时候,参数名称才能和字段名称相同:

public class Circle {    private int x, y, radius;    public void setOrigin(int x, int y) {        ...    }}

Circyle类有字段x,y,radius。方法setOrigin有两个参数,每个参数的名称都和类字段同名。因此在方法体内,使用x,y是使用调用方法时传入的参数值,而不是字段的值。为了访问这些字段,必须用一个全限(qualified)名称,即this,之后章节会加以讨论。

传递原始数据类型参数

原始数据类型参数,如int或double,是通过值传递方式传入方法内部。意味着,参数值的改变只会影响到方法体内部,超出这个范围,参数值的改变也随之消失。例如:

public class PassPrimitiveByValue {    public static void main(String[] args) {        int x = 3;        // invoke passMethod() with         // x as argument        passMethod(x);        // print x to see if its         // value has changed        System.out.println("After invoking passMethod, x = " + x);    }    // change parameter in passMethod()    public static void passMethod(int p) {        p = 10;    }}outputAfter invoking passMethod, x = 3

传递引用数据类型参数

引用数据类型参数,如objects,也是通过值传递方式传入方法内部。这意味着,当方法返回值后,传入的引用仍会引用之前所引用的对象。然而,对象字段的值也可以在方法内部进行更改,假如它们有恰当的访问权限。

例如,某个类的方法moveCircle方法:

public void moveCircle(Circle circle, int deltaX, int deltaY) {    // code to move origin of circle to x+deltaX, y+deltaY    circle.setX(circle.getX() + deltaX);    circle.setY(circle.getY() + deltaY);    // code to assign a new reference to circle    circle = new Circle(0, 0);}

调用方法如下:

moveCircle(myCircle, 23, 56)

调用过程可以分解为两部分:

a. circle的x,y变成23,56.

b. circle改变指向。

方法调用完毕后,myCircle的指向还是没有变化,变化的只是x,y的值。原因是:看似b步骤circle改变了指向,但是方法调用时是引用传递且不能改变。(because the reference was passed in by value and cannot change.)

对象

和你所知道的一样,一个典型的Java程序会创建很多与方法进行交互的对象。通过这些对象的交互,一个程序可以处理很多任务,例如实现一个GUI,运行动画,通过网络发送和接收信息。一旦一个创建出来的对象完成了其任务,它所占用的资源将会被回收,以便其他对象能够利用。

下面是一个小程序,名称为CreateObjectDemo,创建了3个对象:一个Point对象和两个Rectangle对象。你需要三个源码文件来编译这个程序:

public class CreateObjectDemo {    public static void main(String[] args) {        // Declare and create a point object and two rectangle objects.        Point originOne = new Point(23, 94);        Rectangle rectOne = new Rectangle(originOne, 100, 200);        Rectangle rectTwo = new Rectangle(50, 100);        // display rectOne's width, height, and area        System.out.println("Width of rectOne: " + rectOne.width);        System.out.println("Height of rectOne: " + rectOne.height);        System.out.println("Area of rectOne: " + rectOne.getArea());        // set rectTwo's position        rectTwo.origin = originOne;        // display rectTwo's position        System.out.println("X Position of rectTwo: " + rectTwo.origin.x);        System.out.println("Y Position of rectTwo: " + rectTwo.origin.y);        // move rectTwo and display its new position        rectTwo.move(40, 72);        System.out.println("X Position of rectTwo: " + rectTwo.origin.x);        System.out.println("Y Position of rectTwo: " + rectTwo.origin.y);    }}

程序跑起来后,创建了对象,进行了一系列操作,每个对象有不同的值,下面是输出内容:

Width of rectOne: 100Height of rectOne: 200Area of rectOne: 20000X Position of rectTwo: 23Y Position of rectTwo: 94X Position of rectTwo: 40Y Position of rectTwo: 72

接下来的三个部分将用以上例子来描述一个程序内对象的生命周期。从中,你将会学到如何在程序中创建和使用对象。此外你也会学到系统在对象生命周期终结后如何对这些对象进行清理。

创建对象

类为对象提供蓝图(blueprint)。因此,你从一个类中创建对象。以下每条语句都是从CreateObjectDemo程序中创建对象以及赋予其一个变量的:

Point originOne = new Point(23, 94);

Rectangle rectOne = new Rectangle(originOne, 100, 200); //Rectangle是矩形,Point应该是点,即矩形位置

Rectangle rectTwo = new Rectangle(50, 100);

第一行是从Point类中创建出一个对象。第二、三行是从Rectangle类中分别创建出一个对象。

每一条语句包含三个部分:
1. 声明(declaration):粗体代码部分将一个变量名称和对象类型联系起来。
2. 实例化(instantiation):new关键字是创建对象的运算符(operator);
3. 初始化(Initialization):紧接着new关键字的是一个构造函数,调用它,则初始化一个新的对象。

声明变量来引用一个对象

之前,你学到声明一个变量的方式如下:

type name;

这会通知编译器说,你将会用name来引用一个数据类型是type的数据。如果是原始数据类型,这种声明会为变量预留下足够的内存。

你也可以这样定义一个引用变量:

Point originOne;

假如你这样声明,originOne的值不会被确定下来的,除非你创建出一个对象,并赋给它。也就是说,简单声明一个引用时不会创建对象的。为此,你需要使用new运算符(operator)。你必须给originOne赋值后才能使用,否则将会编译错误。

变量在这种没有指向(应用)任何对象状态下,可以使用以下图片解释:

image

实例化一个类

new运算符通过为新对象开辟内存和返回指向内存的引用实例化类。操作符new调用对象构造函数。


术语”instantiating a class:实例化一个类”和”create an Object:创建一个对象”的意思是一样的。当你创建一个对象时,你是在创建一个类的实例,即实例化一个类。


new操作符后面需要一个单一的参数:一个构造函数。构造函数的名称提供要实例化的类名称。

new运算符返回它创建对象的引用,这个引用一般赋值给合适类型的变量,如:

Point originOne = new Point(23, 94);

new运算符所创建出来对象的引用不一定要赋值给一个变量,它可以直接在一个表达式中使用,比如:

int height = new Rectangle().height;

初始化一个对象

下面是Point类的代码:

public class Point {    public int x = 0;    public int y = 0;    //constructor    public Point(int a, int b) {        x = a;        y = b;    }}

这个类包含一个单一的构造函数。你可以很容易识别出一个构造函数,因为它没有返回值,且名称和类的名称一样。Point类的构造函数有两个参数,下面语句为这两个参数提供值:

Point originOne = new Point(23, 94);

其执行结果可以用下图来解释:
image

下面是Rectangle类的代码,包含4个构造函数:

public class Rectangle {    public int width = 0;    public int height = 0;    public Point origin;    // four constructors    public Rectangle() {        origin = new Point(0, 0);    }    public Rectangle(Point p) {        origin = p;    }    public Rectangle(int w, int h) {        origin = new Point(0, 0);        width = w;        height = h;    }    public Rectangle(Point p, int w, int h) {        origin = p;        width = w;        height = h;    }    // a method for moving the rectangle    public void move(int x, int y) {        origin.x = x;        origin.y = y;    }    // a method for computing the area of the rectangle    public int getArea() {        return width * height;    }}

每一个构造函数让你可以为Rectangleorigin变量提供初始值,这个初始值可以是原始类型也可以是引用类型。假如一个类有多个构造函数,那么它们必须有不同的签名(signatures)。Java编译器通过参数的个数和类型来区分这些构造函数。当Java编译器执行到以下代码,它知道要调用的是类Rectangle类内有Point参数和两个Integer类型的构造函数:

Rectangle rectOne = new Rectangle(originOne, 100, 200);

它调用Rectangle中的一个构造函数,并把originOne赋值给origin.同时把100赋值给width,把200赋值给height。现在又两个引用都指向通过一个Point对象——一个对象可以被多个变量所引用。如下图所示:
image

下面的代码调用Rectangle类中有两个Integer参数的构造函数,然后提供为widthheight提供初始值。假如跟踪代码,你会发现它创建了一个xy都是0的Point对象。

Rectangle rectTwo = new Rectangle(50, 100);

下面语句中,构造函数一个参数都没有,因此它叫做无参构造函数(no-argument):

Rectangle rect = new Rectangle();

所有的类至少有一个构造函数,假如某个类不明确声明一个构造函数,Java编译器会自动为这个类提供一个无参构造函数,也被称为默认构造函数。默认构造函数会调用父类的默认构造函数,假如某个类没有父类,那么会调用Object的默认构造函数。假如父类没有构造函数,则编译器会报错。

Object类是所有类的父类。

使用对象

一旦你创建了一个对象,你可能想要用它来做某件事情。你可以使用对象的字段值,改变字段的值,或者调用它的某个方法。

应用某个对象的字段:referencing an Object’s Fields

通过对象的名称可以访问到其字段。因此你必须使用无歧义的名称。

你可能在某个类内使用对象的名称访问它的字段。例如,Rectangle类内访问字段并打印:

System.out.println("Width and height are: " + width + ", " + height);

在这个例子中,widthheight简单名(simple name)。

类外的代码访问字段必须要使用对象引用或表达式,紧接着的是点(dot:.)运算符,最后是简单字段名称如:

objectReference.fieldName

例如:

Rectangle rectOne = new Rectangle();//通过对象引用访问字段System.out.println("Width of rectOne: "  + rectOne.width);System.out.println("Height of rectOne: " + rectOne.height);

如果尝试在类外,比如说在CreateObjectDemo类内使用简单名width和height是没有意义的——这些字段仅存于一个对象——如果使用,其结果是编译错误。

下面是通过表达式返回一个对象引用来访问字段的例子:

//new运算符返回一个对象引用int height = new Rectangle().height;

调用对象的方法:Calling an Object’s methods

你也可以用一个对象引用来调用方法。通过点符号(.)把函数简单名追加进来,之后是括号(),如果函数有任何参数的话要提供相应的参数,如:

objectReference.methodName(argumentList);//有参函数的调用objectReference.methodName();//无参函数的调用

你也可以用任意可以返回对象引用的表达式来调用方法:

new Rectangle(100, 50).getArea()

记住,调用某个对象的方法和向对象发送信息是一样的。


垃圾收集:the garbage collector

一些面向对象语言要求编程者自己追踪所有自己创建出来的对象,并在对象使命完成之后销毁对象,释放资源。管理内存是一件非常无聊且容易出错的事情。Java平台允许你创建任意多的对象,并且你不用担心销毁他们。Java运行环境在侦测到对象不再被使用时,会删掉他们,这种过程被称为垃圾收集(grabage collection);

什么时候被回收

  • 对象不再被被引用;
  • 存储引用的变量超出作用域(out of scope)时,引用被删除;
  • 或者赋予引用变量null值。

Java运行环境有一个垃圾收集器定时地释放掉那些不再被应用的对象所占用的内存。

更多关于类的内容(more on class)

本章节涵盖更多类的内容:
- 从方法中返回值。
- this关键字。
- 类变量 Vs. 实例成员。
- 访问控制。

从方法中返回值

一个方法返回值给调用它的代码,当:
- 完成所有方法中的声明(statement),
- 运行到return语句,或者
- 抛出一个异常。
上面举例的情形,无论哪一个先发生,方法遍返回值。

在方法声明时你会声明该方法的返回类型,在方法内部,你将使用return语句返回值。

任何方法声明为void的语句都不需要包含一个return语句,但也可能这样子做,在这种情况下,一个return可以用来退出方法,如下所示:

public void test(){  //doSomethings  return;//exit the method}

假如方法声明为void,但你返回一个值,则会产生编译错误。

如果方法没有声明为void,则方法必须包含一个return语句并相应地返回一个值:

return returnValue;

返回值的数据类型必须要和方法声明返回的数据类型一致,否则产生编译错误。

方法可以返回原始数据类型的值,也可以返回引用数据类型的值,如:

  // a method for computing the area of the rectangle    public int getArea() {        return width * height;    }public Bicycle seeWhosFastest(Bicycle myBike, Bicycle yourBike,                              Environment env) {    Bicycle fastest;    // code to calculate which bike is     // faster, given each bike's gear     // and cadence and given the     // environment (terrain and wind)    return fastest;}

第一个方法返回原始数据类型,第二个方法返回引用数据类型。**

返回一个类或者接口

当一个方法使用一个类名当做它的返回值,例如WhosFaster,则方法的返回值必须是该类型的或者是它的子类型。假设你有以下类层次结构的类:ImaginaryNumberNumber类的子类,是Object的子类,可以用下图解释:

The class hierarchy for ImaginaryNumber

现在假设你有一个返回一个类型为Number的方法:

public Number returnANumber(){

...

}

returnANumber方法可以返回一个ImaginaryNumber,但不能是ObjectImaginationNumber是一个Number,然和Object却并不一定是一个Number,它可能是String等任意类型的。

使用this关键字

在一个实例方法(instance method)或者一个构造函数内部,关键字this是当前对象的一个引用——即该方法或者构造函数从属的对象。在方法或者构造函数内部,你可以通过使用this关键字访问到当前对象的任何成员。

使用this引用字段

使用this关键字最普遍的理由就是字段被方法或者构造函数的参数所遮蔽(shadowed).例如:

public class Point {    public int x = 0;    public int y = 0;    //constructor    public Point(int x, int y) {        this.x = x;        this.y = y;    }}

每一个参数都遮蔽了类的其中一个字段。为了引用到Point的字段x,构造函数内必须使用this.x

用this关键字调用构造函数

在构造函数内部,你可以使用this关键字访问同一个类内部的其他构造函数,Java编译器通过构造函数的参数个数和类别来区分它们,如:

public class Rectangle {    private int x, y;    private int width, height;    public Rectangle() {        this(0, 0, 1, 1);//调用的是有四个参数的构造函数    }    public Rectangle(int width, int height) {        this(0, 0, width, height);//调用的是有四个参数的构造函数    }    public Rectangle(int x, int y, int width, int height) {        this.x = x;        this.y = y;        this.width = width;        this.height = height;    }    ...}

该类包含了一系列的构造函数。每一个构造函数初始化了一部分或者全部的Rectangle的成员变量。

假如构造函数内部用this调用其它构造函数,则调用的代码必须出现出现在第一行。

类成员的访问控制权限(Controlling Access to members of a Class)

访问级别(Access Level)修饰符决定了一个类是佛可以使用一个特殊的字段或方法。这里有2个级别的访问控制:
- 最高级别(At the top level)——public或者包私有(package-private即no-modifier)。
- 成员级别(At the member level)—— public,private,protected,no-modifier。
可能用public来声明一个类,意味着该类对任意位置的任意类可见。假如一个类没有修饰词,则它仅在同一个包内(包的命名来源于一组相关类)可见。

在成员等级找那个,你也可以使用和public修饰符或默认修饰符,就像最高等级访问修饰符一样,其意思也一样。对于成员而言,还有额外的访问修饰符:privateprotected.private修饰符明确了成员只能在类内被访问到。protected修饰词明确成员仅能在其包内被访问,此外,它也能被包外的子类所访问。

下表显示了在每种修饰词作用下成员被访问的许可

Modifier Class Package Subclass World public Yes Yes Yes Yes protected Yes Yes Yes No no modifier Yes Yes No No private Yes No No No

1. 第一列表明类是否可以访问由不同访问级别定义的成员。正如你所看到的那样:类总是可以访问它自己的成员
2. 第二列表明同一个包下的类(不必去管类之间的关系,如父类与子类等)是否可以访问成员。
3. 第三列表明在包外声明的子类是否可以访问成员。
4. 第四列表明是否所有的类可以访问成员。

访问级别可以从以下两个方面影响你。第一,当你使用其他来源的包,比如说Java平台的包,访问级别决定了你的类能用那些其他包内类的成员。第二,当你编写一个类时,你需要决定类内成员该有的访问等级。

下面用图解析访问等级。

classes-access

下表显示了Alpha成员在不同访问等级修饰词作用下的可见性.

Modifier Alpha Beta AlphaSub Gamma public Yes Yes Yes Yes protected Yes Yes Yes No no modifier Yes Yes No No private Yes No No No

选择访问等级的一些建议
如果接口对外开放,而你又不想因消费方的滥用而出现问题,访问权限修饰词可以帮你。
- 使用最严格的访问权限修饰词private,除非你有不那么用的更好的理由。
- 避免成员变量使用public修饰(Constant:常量例外)。因为public字段会和你的实现联系起来,这不利于你灵活地改变代码,而且该成员字段还可以被消费方随意篡改。

理解类成员(Understanding Class Members)

在本章节中,我们讨论用static关键字来创建属于类而不是类实例的字段和方法。

类变量

由同一个类创建出来的许多对象,它们的实例变量是不一样的。在Bicycle例子中,实例变量是cadence,gear,和speed。每一个Bicycle对象的变量有它自己的值,存储的位置也不一样。

有时, 你期望一些变量对于所有的对象来说是一样的,普通的(不具备特殊性)。用static修饰词可以实现。用static声明的字段(Field)被称为静态字段(static fields)或者类变量(class variables)。它们与类关联而不是对象。类变量在内存中位置是固定且仅存一份,意味着类的实例共享着同一个类变量。任意对象可以访问和修改类变量,但不创建对象情况下也是可以办到的。

例如,假设你想记录Bicycle生产出来的一系列自行车的序列号,如下:

public class Bicycle {    private int cadence;    private int gear;    private int speed;    // add an instance variable for the object ID    private int id;    // add a class variable for the    // number of Bicycle objects instantiated    private static int numberOfBicycles = 0;//从0开始        ...}/* 实现方式,在构造函数中递增numberOfBicycles(以为构造函数是实例化的入口)*/   public Bicycle(int startCadence, int startSpeed, int startGear){        gear = startGear;        cadence = startCadence;        speed = startSpeed;        // increment number of Bicycles        // and assign ID number        id = ++numberOfBicycles;    }

用类名引用类变量,如:

Bicycle.numberOfBicycles


你也可以用Bicycle实例来引用,如myBike.numberOfBicycles。但是不鼓励这样做(不好区分是实例变量还是类变量,如果用类名引用就不一样了).


类方法(Class method)

和类变量一样,Java编程语言也支持类方法,即用static声明方法。调用的时候应该用类名来调用,如:

ClassName.method();//类名调用instanceName.method();//也可以通过实例来调用,但是不建议

一个使用static方法的场景就是为了访问static字段。如:

public static int getNumberOfBicycles() {    return numberOfBicycles;}

方法(包含实例方法,类方法)和变量(包含实例变量和类变量)组合需要符合以下规则:

  • 实例方法可以直接访问实例变量或实例方法。
  • 实例方法可以直接访问类变量和类方法。
  • 类方法可以直接访问类变量和类方法。
  • 类方法不能直接访问实例变量或实例方法——它们必须使用一个对象引用。同理,类方法不能使用this关键字,因为没有this所引用的实例。

常量

staticfinal关键字组合一起,可以定义常量。final意味着变量不可变。如:

static final double PI = 3.141592653589793;

常量如果更改其值,会产生编译错误。按照约定,常量的命名规则是全部大写,用下划线分隔开。

初始化字段(Initializing Fields)

我们经常在声明变量的同时就赋初值:

public class BedAndBreakfast {    // initialize to 10    public static int capacity = 10;    // initialize to false    private boolean full = false;}

这种方式既简单又有用,然而,因其简单性(声明和初始化只能放在同一行)这种初始化方式不支持初始值需要经过逻辑处理后才获取到的方式。实例变量可以在能进行异常捕获等其他逻辑处理的构造函数中初始化。为了给实例变量提供一样的功能,Java编程语言包含了静态初始化块(static initialization blocks)。


虽然很常见,但没必要在声明变量的同时初始化。唯一需要这样做的时候是在使用之前初始化。


静态初始化块

静态初始化块是在{}里面的一个正常块,用static关键字修饰:

static {    // whatever code is needed for initialization goes here}

一个类可以拥有很多个静态初始化块,并且可以处于类内任意位置。运行系统保证这些静态初始化块的初始化的执行顺序和源码中出现这些块的顺序一致。

下面是静态初始化块的替代:

class Whatever {    public static varType myVar = initializeClassVariable();    private static varType initializeClassVariable() {        // initialization code goes here    }}

私有静态方法(private static method)的好处是你可以重新调用它来赋值。

初始化实例变量(Initializing instance Members)

正常情况下,你会用构造函数来初始化实例变量。此外有两种替代方式来初始化实例变量:初始化块(Initialize blocks)和不可变方法(final methods).

初始化块和静态初始化块看起来差不多,只不过少了static关键字:

{    // whatever code is needed for initialization goes here}

Java编译器会把每个初始化块复制到构造函数中。因此,多个构造函数之间可以用这种方式来共享代码块。

在子类内,不可重写不可变方法(final methods)。下面是利用不可变方法来初始化实例变量:

class Whatever {    private varType myVar = initializeInstanceVariable();    protected final varType initializeInstanceVariable() {        // initialization code goes here    }}

如果子类想重用初始化方法,这种方式特别有用。方法是不可变的原因是实例化过程中调用可变方法可以产生问题。

理解类成员(Understanding Class Members)

在本章节中,我们讨论用static关键字来创建属于类而不是类实例的字段和方法。

类变量

由同一个类创建出来的许多对象,它们的实例变量是不一样的。在Bicycle例子中,实例变量是cadence,gear,和speed。每一个Bicycle对象的变量有它自己的值,存储的位置也不一样。

有时, 你期望一些变量对于所有的对象来说是一样的,普通的(不具备特殊性)。用static修饰词可以实现。用static声明的字段(Field)被称为静态字段(static fields)或者类变量(class variables)。它们与类关联而不是对象。类变量在内存中位置是固定且仅存一份,意味着类的实例共享着同一个类变量。任意对象可以访问和修改类变量,但不创建对象情况下也是可以办到的。

例如,假设你想记录Bicycle生产出来的一系列自行车的序列号,如下:

public class Bicycle {    private int cadence;    private int gear;    private int speed;    // add an instance variable for the object ID    private int id;    // add a class variable for the    // number of Bicycle objects instantiated    private static int numberOfBicycles = 0;//从0开始        ...}/* 实现方式,在构造函数中递增numberOfBicycles(以为构造函数是实例化的入口)*/   public Bicycle(int startCadence, int startSpeed, int startGear){        gear = startGear;        cadence = startCadence;        speed = startSpeed;        // increment number of Bicycles        // and assign ID number        id = ++numberOfBicycles;    }

用类名引用类变量,如:

Bicycle.numberOfBicycles


你也可以用Bicycle实例来引用,如myBike.numberOfBicycles。但是不鼓励这样做(不好区分是实例变量还是类变量,如果用类名引用就不一样了).


类方法(Class method)

和类变量一样,Java编程语言也支持类方法,即用static声明方法。调用的时候应该用类名来调用,如:

ClassName.method();//类名调用instanceName.method();//也可以通过实例来调用,但是不建议

一个使用static方法的场景就是为了访问static字段。如:

public static int getNumberOfBicycles() {    return numberOfBicycles;}

方法(包含实例方法,类方法)和变量(包含实例变量和类变量)组合需要符合以下规则:

  • 实例方法可以直接访问实例变量或实例方法。
  • 实例方法可以直接访问类变量和类方法。
  • 类方法可以直接访问类变量和类方法。
  • 类方法不能直接访问实例变量或实例方法——它们必须使用一个对象引用。同理,类方法不能使用this关键字,因为没有this所引用的实例。

常量

staticfinal关键字组合一起,可以定义常量。final意味着变量不可变。如:

static final double PI = 3.141592653589793;

常量如果更改其值,会产生编译错误。按照约定,常量的命名规则是全部大写,用下划线分隔开。

初始化字段(Initializing Fields)

我们经常在声明变量的同时就赋初值:

public class BedAndBreakfast {    // initialize to 10    public static int capacity = 10;    // initialize to false    private boolean full = false;}

这种方式既简单又有用,然而,因其简单性(声明和初始化只能放在同一行)这种初始化方式不支持初始值需要经过逻辑处理后才获取到的方式。实例变量可以在能进行异常捕获等其他逻辑处理的构造函数中初始化。为了给实例变量提供一样的功能,Java编程语言包含了静态初始化块(static initialization blocks)。


虽然很常见,但没必要在声明变量的同时初始化。唯一需要这样做的时候是在使用之前初始化。


静态初始化块

静态初始化块是在{}里面的一个正常块,用static关键字修饰:

static {    // whatever code is needed for initialization goes here}

一个类可以拥有很多个静态初始化块,并且可以处于类内任意位置。运行系统保证这些静态初始化块的初始化的执行顺序和源码中出现这些块的顺序一致。

下面是静态初始化块的替代:

class Whatever {    public static varType myVar = initializeClassVariable();    private static varType initializeClassVariable() {        // initialization code goes here    }}

私有静态方法(private static method)的好处是你可以重新调用它来赋值。

初始化实例变量(Initializing instance Members)

正常情况下,你会用构造函数来初始化实例变量。此外有两种替代方式来初始化实例变量:初始化块(Initialize blocks)和不可变方法(final methods).

初始化块和静态初始化块看起来差不多,只不过少了static关键字:

{    // whatever code is needed for initialization goes here}

Java编译器会把每个初始化块复制到构造函数中。因此,多个构造函数之间可以用这种方式来共享代码块。

在子类内,不可重写不可变方法(final methods)。下面是利用不可变方法来初始化实例变量:

class Whatever {    private varType myVar = initializeInstanceVariable();    protected final varType initializeInstanceVariable() {        // initialization code goes here    }}

如果子类想重用初始化方法,这种方式特别有用。方法是不可变的原因是实例化过程中调用可变方法可以产生问题。

嵌套类(Nested Class)

Java编程语言允许你在一个类内定义其它类。如:

class OuterClass {    ...    class NestedClass { //内部类        ...    }}

术语:嵌套类分为两类:静态的和非静态的(non-static)。用static声明的的嵌套类被称为静态嵌套类。非静态嵌套类被称为内部类。


class OuterClass {    ...    static class StaticNestedClass {//静态嵌套类        ...    }    class InnerClass {//非静态内部类,内部类        ...    }}

一个嵌套类是其外围类(enclosing Class)的成员。内部类可以访问其外围类的所有成员,包括私有成员。静态嵌套类不能访问其外围类的的成员(类变量不属于类的成员)。作为外部类(外围类)的成员,嵌套类可以声明为private,public,protected或缺省

为何要使用嵌套类

令人信服的两个理由包括:

  • 对于只在一个地方使用到类来说,这是一种逻辑分组的方法。假如一个类只对某一个类有用,则将它嵌入该类是合乎逻辑的且可以让它们放在一起。嵌套这样的”帮助类”让它们所处的包更加合理化。

  • 这增强了封装特性。考虑到A和B两个顶层类,B需要访问A的私有成员。此外B它自己也能够对外隐藏自己。(A是外部类,B是内部类)

  • 可读性更强,维护性更佳。在顶层类(top-level class)内嵌套类使代码更加接近调用方。

静态嵌套类

和类方法、类变量一样,静态嵌套类与它的外围类相关联。和类方法类似,静态嵌套类不能直接引用外部类的实例变量或者实例方法,它们可以通过对象引用被使用。


静态嵌套类与外部类的成员,或者其任意类的交互是一样的。实际上,表现得像顶层类的静态嵌套类被嵌入到其他顶层类内是为了易于打包


静态嵌套类通过外部类的名称访问:
OuterClassName.StaticNestedClass

例如创建静态嵌套类对象如下:

OuterClass.StaticNestedClass nestedObject =     new OuterClass.StaticNestedClass();

内部类

和实例变量、方法一样,内部类与其外部类相关联,且可以访问外部类对象的方法和字段。此外,因为内部类是与一个实例相关联的,所以它自己不能定义任何静态成员。

内部类的实例仅存在于外部类的实例之内。请看一下例子:

class OuterClass {    ...    class InnerClass {        ...    }}

内部类的实例仅存在于外部类的实例之内,它可以直接访问外部类的函数和字段。

为了实例化内部类,你必须先实例化内部类。然后用一下语法实例化内部类

OuterClass.InnerClass innerObject = outerObject.new InnerClass();

有两种特别的内部类:局部内部类和匿名内部类。(local classes and anonymous class)

遮蔽(Shadowing)

假如在一个特殊的作用范围(如内部类、方法定义中),声明一种类型(如成员变量或者参数名称)的变量和其外部类一样,则这种声明会遮蔽掉外部类的声明。引用同类型、同名称的外部类成员,不能仅用其名,如:

public class ShadowTest {    public int x = 0;    class FirstLevel {        public int x = 1;        void methodInFirstLevel(int x) {            System.out.println("x = " + x);            System.out.println("this.x = " + this.x);            System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);        }    }    public static void main(String... args) {        ShadowTest st = new ShadowTest();        ShadowTest.FirstLevel fl = st.new FirstLevel();        fl.methodInFirstLevel(23);    }}outputx = 23this.x = 1ShadowTest.this.x = 0

该例子中定义了三个叫x的变量:一个是ShadowTest 的成员变量;一个是内部类FirstLevel 的成员变量;一个是methodinFirstLevel的参数。

methodinFirstLevel的参数遮蔽了FirstLevel 的成员变量,想要访问FirstLevel 的成员变量,需要加上this关键字,表示闭合的作用范围(enclosing scope)。

引用闭合作用范围更大的成员变量需要用可以表示其从属关系的类名。例如:
System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);

序列化(Serialization)

不鼓励对内部类(局部内部类,匿名内部类)进行序列化。由于不同的Java编译器实现不同,会存在存在兼容问题。

内部类例子(Inner Class Example)

为了弄懂内部类的使用,首选数组。在以下例子中,你创建一个数组,用Integer值填充,然后升序输出偶数(even)索引的数组值.

DataStructure.java由以下3部分组成:

  • DataStructure外部类,包含一个构造函数(创建一个数组,用自然数填充)和一个打印数组偶数索引的数组值。
  • EvenIterator内部类,它实现了接口DataStructureIterator。迭代器(iterators)用于遍历数据结构,典型地,它拥有测试是否是最后一个元素,检索当前元素,以及移动到下一个元素的方法。
  • 一个main函数,它实例化了DataStructure类,然后调用printEven方法来升序输出arrayOfInts索引为偶数的值。
public class DataStructure {    // Create an array    private final static int SIZE = 15;    private int[] arrayOfInts = new int[SIZE];    public DataStructure() {        // fill the array with ascending integer values        for (int i = 0; i < SIZE; i++) {            arrayOfInts[i] = i;        }    }    public void printEven() {        // Print out values of even indices of the array        DataStructureIterator iterator = this.new EvenIterator();        while (iterator.hasNext()) {            System.out.print(iterator.next() + " ");        }        System.out.println();    }    interface DataStructureIterator extends java.util.Iterator<Integer> { }     // Inner class implements the DataStructureIterator interface,    // which extends the Iterator<Integer> interface    private class EvenIterator implements DataStructureIterator {        // Start stepping through the array from the beginning        private int nextIndex = 0;        public boolean hasNext() {            // Check if the current element is the last in the array            return (nextIndex <= SIZE - 1);        }                public Integer next() {            // Record a value of an even index of the array            Integer retValue = Integer.valueOf(arrayOfInts[nextIndex]);            // Get the next even element            nextIndex += 2;            return retValue;        }    }    public static void main(String s[]) {        // Fill the array with integer values and print out only        // values of even indices        DataStructure ds = new DataStructure();        ds.printEven();    }}The output is:0 2 4 6 8 10 12 14 

请注意,EvenIterator类是直接引用DataStructure对象内的arrayOfInts实例变量的。

你可以使用内部类来实现帮助类的实现,如本例子所描述的。

局部内部类和匿名内部类

有额外的两种内部类。你可以在一个方法体内声明一个内部类,这种内部类称为局部内部类(local class) 。你也可以在方法体内定义一个没有名字的内部类,这种内部类称为匿名内部类(anonymous class).

修饰符(Modifiers)

你可以使用外部类对成员所用的修饰符,如private,public,protected和no-modified来限制对内部类的访问,就像使用它们限制类成员的访问一样。如上面例子中,字段arrayOfInts和内部类EvenIterator如果有一样的修饰符,那么它们对外可见性是一样的。

局部内部类(Local Classes)

局部内部类是在块内定义的,它是花括号({})内一组0个或多个语句。典型地,你会发现局部内部类(本地类)在方法体内定义。

声明局部内部类(Declaring Local Classes)

你可以在任意块内定义一个本地类。例如你可以在方法体内,for循环内,或者if字句内定义局部内部类。

LocalClassExample类内有一个PhoneNumber方法,它验证两个PhoneNumber

public class LocalClassExample {    static String regularExpression = "[^0-9]";    public static void validatePhoneNumber(        String phoneNumber1, String phoneNumber2) {        final int numberLength = 10;        // Valid in JDK 8 and later:        // int numberLength = 10;        class PhoneNumber {            String formattedPhoneNumber = null;            PhoneNumber(String phoneNumber){                // numberLength = 7;                String currentNumber = phoneNumber.replaceAll(                  regularExpression, "");                if (currentNumber.length() == numberLength)                    formattedPhoneNumber = currentNumber;                else                    formattedPhoneNumber = null;            }            public String getNumber() {                return formattedPhoneNumber;            }            // Valid in JDK 8 and later://            public void printOriginalNumbers() {//                System.out.println("Original numbers are " + phoneNumber1 +//                    " and " + phoneNumber2);//            }        }        PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1);        PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2);        // Valid in JDK 8 and later://        myNumber1.printOriginalNumbers();        if (myNumber1.getNumber() == null)             System.out.println("First number is invalid");        else            System.out.println("First number is " + myNumber1.getNumber());        if (myNumber2.getNumber() == null)            System.out.println("Second number is invalid");        else            System.out.println("Second number is " + myNumber2.getNumber());    }    public static void main(String... args) {        validatePhoneNumber("123-456-7890", "456-7890");    }}outputFirst number is 1234567890Second number is invalid

验证一个手机号码,首先先过滤掉非数字字符;然后验证手机号码包含10个数字(北美手机号码长度)。

访问外围类的成员(Accessing Members of an Enclosing Class)

局部内部类有权限访问其外围类的成员。在前面的例子中,PhoneNumber 构造函数访问LocalClassExample的成员regularExpression

此外,局部内部类可以访问局部变量(即方法内的变量)。然而,局部内部类只能访问被声明为final的局部变量。当一个局部内部类访问一个局部变量或者外围块的参数时,它捕获那个变量或者参数。例如,PhoneNumber构造函数可以访问局部变量numberLength,因为它声明为final。numberLength是一个捕获变量。

然而,从Java SE 8开始,本地内部类可以访问局部变量和外围块的参数是final或者effectively final的。一个变量或者参数初始化后就不再改变就是Effective final。例如下面代码中numberLength声明为非final,你在在PhoneNumber构造函数中这样使用:

PhoneNumber(String phoneNumber) {    numberLength = 7;//报错    String currentNumber = phoneNumber.replaceAll(        regularExpression, "");    if (currentNumber.length() == numberLength)//报错        formattedPhoneNumber = currentNumber;    else        formattedPhoneNumber = null;}

因为numberLength = 7是赋值语句,所以会报错,报错信息为Variable numberLength is accessed from an inner class,needs must be final or effectively final


A variable or parameter whose value is never changed after it is initialized is effectively final.


从Java SE 8开始,假如你在方法内定义一个局部内部类,它可以访问方法的参数,如,你可以在PhoneNumber局部内部类内定义如下方法:

public void printOriginalNumbers() {    System.out.println("Original numbers are " + phoneNumber1 +        " and " + phoneNumber2);}

局部内部类和内部类相似(Local Classes Are Similar To Inner Classes)

局部内部类和内部类相似是因为他们都不可以定义或者声明任何静态成员。在静态方法内部的局部内部类,如局部内部类PhoneNumber,只能引用外部类的静态成员。如果引用的不是静态成员,则会报错Non-static field 'regularExpression' cannot be referenced from a static context.

局部内部类是非静态的(non-static)是因为它们可以访问外围块的实例成员。因此,它们不能包含大多数静态声明。

不能在一个块内声明一个接口,因为接口本质上是静态的。例如,下面代码编译不通过:

public void greetInEnglish() {        interface HelloThere { //块内不能定义接口           public void greet();        }        class EnglishHelloThere implements HelloThere {            public void greet() {                System.out.println("Hello " + name);            }        }        HelloThere myGreeting = new EnglishHelloThere();        myGreeting.greet();    }

你不能在局部内部类内声明接口或者静态初始化块。以下代码编译不通过,因为EnglishGoodbye.sayGoodbye声明为static。编译错误信息大概是modifier 'static' is only allowed in constant variable declaration.

public void sayGoodbyeInEnglish() {        class EnglishGoodbye {            public static void sayGoodbye() {//不能声明为static                System.out.println("Bye bye");            }        }        EnglishGoodbye.sayGoodbye();    }

局部内部类可以拥有常量(static final)。常量是原始类型的数据或者声明为final且在编译时期值就确定好的String。下面代码编译通过是因为EnglishGoodbye.farewell是一个常量。

 public void sayGoodbyeInEnglish() {        class EnglishGoodbye {            //静态成员变量必须为常量才行,否则报错            public static final String farewell = "Bye bye";            public void sayGoodbye() {                System.out.println(farewell);            }        }        EnglishGoodbye myEnglishGoodbye = new EnglishGoodbye();        myEnglishGoodbye.sayGoodbye();    }

匿名内部类(Anonymous Classes)

匿名内部类让你的代码更加简练。它能让你在声明类的同时实例化类。除了没有名字之外它和局部内部类一样。当你只需要用到一个局部内部类一次的话,请使用匿名内部类

声明匿名内部类

局部内部类是类的声明,然而,匿名内部类是表达式,意味着你在其他表达式中定义类。以下例子中,HelloWorldAnonymousClasses使用匿名内部类初始化局部变量frenchGreetingspanishGreeting。用一个局部内部类来初始化变量englishGreeting.

public class HelloWorldAnonymousClasses {    interface HelloWorld {        public void greet();        public void greetSomeone(String someone);    }    public void sayHello() {        class EnglishGreeting implements HelloWorld {//局部内部类            String name = "world";            public void greet() {                greetSomeone("world");            }            public void greetSomeone(String someone) {                name = someone;                System.out.println("Hello " + name);            }        }        HelloWorld englishGreeting = new EnglishGreeting();        HelloWorld frenchGreeting = new HelloWorld() { //匿名内部类            String name = "tout le monde";            public void greet() {                greetSomeone("tout le monde");            }            public void greetSomeone(String someone) {                name = someone;                System.out.println("Salut " + name);            }        };        HelloWorld spanishGreeting = new HelloWorld() {//匿名内部类            String name = "mundo";            public void greet() {                greetSomeone("mundo");            }            public void greetSomeone(String someone) {                name = someone;                System.out.println("Hola, " + name);            }        };        englishGreeting.greet();        frenchGreeting.greetSomeone("Fred");        spanishGreeting.greet();    }    public static void main(String... args) {        HelloWorldAnonymousClasses myApp =            new HelloWorldAnonymousClasses();        myApp.sayHello();    }            }

匿名内部类的语法

之前提到过,匿名内部类是表达式。匿名内部类的语法和调用构造函数差不多,除了在代码块中有类的定义。

frenchGreeting例子来说明:

 HelloWorld frenchGreeting = new HelloWorld() {            String name = "tout le monde";            public void greet() {                greetSomeone("tout le monde");            }            public void greetSomeone(String someone) {                name = someone;                System.out.println("Salut " + name);            }        };

匿名内部类表达式由以下几部分组成:

  • new操作符。
  • 要实现的接口名称或者要继承的类名称。在本例子中,匿名内部类实现了HelloWorld接口。
  • 包含传递给构造函数参数的圆括号。注意:当匿名内部类实现一个接口是没有构造函数的,所以你用空的圆括号,如上面代码。
  • 一个类声明体。具体一点,声明体内,方法声明时允许的但语句不允许(but statement is not:估计是抽象函数不允许。)。

因为一个匿名内部类的定义是一个表达式,它必须是语句的一部分。在本例子中,匿名内部类表达式是语句的一部分(花括号后面有分号)。

访问外部类的局部变量以及声明和访问匿名内部类的成员

和局部内部类一样,匿名内部类可以捕获变量(capture variables),它们具有一样的权限访问外部类的局部变量:

  • 匿名内部类可以访问外部类的成员。
  • 匿名内部类不可以访问封闭范围内被声明为非final或非effective final的局部变量。
  • 和嵌套类一样,如果匿名内部类中声明的变量名称和封闭范围(enclosing scope)内声明的变量名称一样,则匿名内部类内的变量会遮蔽封闭范围内的同名变量。

匿名内部类和局部内部类的成员具有一样的限制:

  • 你不能声明静态初始化或者在一个匿名内部类中声明一个接口。

  • 除了常量,匿名内部类不能有静态成员。

注意,你可以在匿名内部类内进行以下声明:

  • 字段(Fields)

  • 额外方法,即使所实现的接口中不包含在内。

  • 实例初始化(Instance initializers)

  • 局部内部类。

你不能在匿名内部类中声明构造函数。(无名称,无法声明)

匿名内部类的例子

import javafx.event.ActionEvent;import javafx.event.EventHandler;import javafx.scene.Scene;import javafx.scene.control.Button;import javafx.scene.layout.StackPane;import javafx.stage.Stage;public class HelloWorld extends Application {    public static void main(String[] args) {        launch(args);    }    @Override    public void start(Stage primaryStage) {        primaryStage.setTitle("Hello World!");        Button btn = new Button();        btn.setText("Say 'Hello World'");        btn.setOnAction(new EventHandler<ActionEvent>() {            @Override            public void handle(ActionEvent event) {                System.out.println("Hello World!");            }        });        StackPane root = new StackPane();        root.getChildren().add(btn);        primaryStage.setScene(new Scene(root, 300, 250));        primaryStage.show();    }}

Lambda 表达式(Lambda Expressions)

匿名内部类有一个问题就是,假如匿名内部类的实现很简单,比如说一个接口只有一个方法,那匿名内部类就显得不清晰,其十分笨重。在这种情况下,你通常会试着把功能(functionality)作为另一个函数的参数传递过去。比如说,当按下一个按钮,应该会采取什么行动。lambda表达式可以让你做到这一点,即将功能作为方法参数,或者说代码当成数据。

匿名内部类章节,给我们展示了如何不用名字也能实现一个接口。尽管这比有名字的类更加简洁,但对于只有一个函数的类来说,匿名内部类还是显得有些过多,笨重。lambda让你更简洁地表达只有一个方法的类实例

诠释Lambda的完美例子

假设你正在开发一个网络应用程序。你想要实现这样一个功能:允许管理员实现任意操作,例如给满足一定条件的成员发送一条信息。详情请看下表的用例:

领域 描述 用例名称 对所选中的成员进行一些操作 主要角色 管理员 前置条件 管理员已登录系统 后置条件 已经对满足条件的成员执行了操作 主要的成功场景 管理员指定待执行操作成员的条件。2. 管理员指定所要执行的操作。 3. 管理员点击提交按钮。4. 系统找到满足条件的成员。5. 系统对满足成员执行一些操作。 扩展用例 1a. 管理员有个可选的操作,即在执行特定操作时,先预览满足条件的成员。 发生的频率 一天好多次

假设社交网络应用成员的成员表示如下:

import lombok.Data;import lombok.NoArgsConstructor;@NoArgsConstructor@Datapublic class Person implements Cloneable{    public enum Sex{        MALE,FEMALE;    }   public String name;    public int age;    public Sex gender;    public String emailAddress;    public void printPerson(){        System.out.println(toString());    }    @Override    protected Object clone(){        try {            return super.clone();        } catch (CloneNotSupportedException e) {            throw new RuntimeException(e.getMessage());        }    }}

假设成员都放在List实例内。

本章节先以一个简单的方法来实现。然后,逐步地用本地类和匿名内部类来改善,然后以简练的Lambda表达式结束。

方法一:创建一个满足一个特征的成员

最简单的例子就是创建很多方法,每个方法筛选出一个特征的成员,比如年龄或者性别。以下例子是满足年纪大于某个值的成员:

public static void printPersonsOlderThan(List<Person> roster, int age) {    for (Person p : roster) {        if (p.getAge() >= age) {            p.printPerson();        }    }}

这种方法使你的程序脆弱不堪,比如引入新的需求(数据类型改变)。假设你升级了你的应用程序,即是说改变Person的数据结构使它包含不同的成员变量;也许测量年龄的算法已经改变。你不得不重写很多API,此外不应该加上这种限制。假如说要你想把处于某个年龄阶段的成员输出呢?

方法二:创建更加普遍(Generalized)的搜索方法

以下方法比方法一更加通用,它打印出年龄段在某个范围内的成员:

public static void printPersonsWithinAgeRange(    List<Person> roster, int low, int high) {    for (Person p : roster) {        if (low <= p.getAge() && p.getAge() < high) {            p.printPerson();        }    }}

假如你想打印出具体性别的成员或者打印出年龄在某个范围以及与具体性别的成员?假如Person类增加了其他属性例如情感状态或者地理位置?虽然以上例子比方法一更加通用,但尝试着为每种可能的搜索条件编写方法,还是使得代码容脆弱不堪。你可以在不同的类中编写查询条件不同的代码。

方法三:在内部类中指定具体化查询条件

//接口interface CheckPerson {    boolean test(Person p);}//内部类class CheckPersonEligibleForSelectiveService implements CheckPerson {    public boolean test(Person p) {        return p.gender == Person.Sex.MALE &&            p.getAge() >= 18 &&            p.getAge() <= 25;    }}public static void printPersons(    List<Person> roster, CheckPerson tester) {    for (Person p : roster) {        if (tester.test(p)) {            p.printPerson();        }    }}//调用示例printPersons(    roster, new CheckPersonEligibleForSelectiveService());

方法三比方法1,2要好得多——假如Person结构改变了,你不需要重写方法。但是你仍旧要编写额外的代码:一个新的接口和一个实现特殊搜索条件的本地类实现类。因为CheckPersonEligibleForSelectiveService实现了一个接口,所以你可以使用匿名内部类,而不是本地类,这样就绕过了为每种查询条件都创建出一个新的本地类了。

方法四:用匿名内部类来实现标准查询

printPersons(    roster,    new CheckPerson() {        public boolean test(Person p) {            return p.getGender() == Person.Sex.MALE                && p.getAge() >= 18                && p.getAge() <= 25;        }    });

这种方法比方法三要好的地方是,节省了很多代码,即不用为每一种搜索声明一个本地类。但是对于每个接口只有一个方法的情况来看,匿名内部类的语法十分笨重,体积庞大。这种情况下,你可以使用Lambda表达式来替代匿名内部类。

方法五:用Lambda表达式来指定查询条件

//这是一个方法调用,调用的是方法printPersons(List<Person> roster,CheckPerson checkPerson)printPersons(    roster,    new CheckPerson() {        public boolean test(Person p) {            return p.getGender() == Person.Sex.MALE                && p.getAge() >= 18                && p.getAge() <= 25;        }    });

用标准的方法接口来替代接口CheckPerson将进一步节省代码。

方法六:Lambda表达式与标准函数接口(standard Functional)方法结合使用

JDK在包java.util.function内定义了几种标准的函数接口。如你可以用Predicate来替代接口CheckPerson,它里面包含的方法有:

interface Predicate<T> {    boolean test(T t);}

于是

//方法定义,现在用的是Predicate接口public static void printPersonsWithPredicate(    List<Person> roster, Predicate<Person> tester) {    for (Person p : roster) {        if (tester.test(p)) {            p.printPerson();        }    }}//调用printPersonsWithPredicate(    roster,    p -> p.getGender() == Person.Sex.MALE        && p.getAge() >= 18        && p.getAge() <= 25);

不仅在这个方法内使用一个Lambda表达式,以下方法建议用其他方法使用Lambda表达式。

方法七:在你的应用程序中使用Lambda表达式

public static void printPersonsWithPredicate(    List<Person> roster, Predicate<Person> tester) {    for (Person p : roster) {        if (tester.test(p)) {            p.printPerson();        }    }}

观察以上代码,我们发现可以用其他操作替代printPerson()方法,可以用Lambda来具体化这种操作。假设你想用类似于printPerson()方法的Lambda表达式,在java.util.function包下可以发现以下接口可以胜任:

public interface Consumer<T> {    /**     * Performs this operation on the given argument.     *     * @param t the input argument     */    void accept(T t);//返回值为void}

改造后的方法变成

public static void processPersons(    List<Person> roster,    Predicate<Person> tester,    Consumer<Person> block) {        for (Person p : roster) {            if (tester.test(p)) {                block.accept(p);            }        }}//调用方式为processPersons(     roster,//List<Person> roster     p -> p.getGender() == Person.Sex.MALE         && p.getAge() >= 18         && p.getAge() <= 25,//Predicate<Person> tester     p -> p.printPerson()  //Consumer<Person> block);

如果你想在满足特定条件的成员(person)上检索联系信息,如email。则可以发现java.util.fuction包下有个方法:

public interface Function<T, R> {    /**     * Applies this function to the given argument.     *     * @param t the function argument     * @return the function result     */    R apply(T t);}

于是方法变成

processPersonsWithFunction(    roster,    p -> p.getGender() == Person.Sex.MALE        && p.getAge() >= 18        && p.getAge() <= 25,    p -> p.getEmailAddress(),// R apply(T t);t是方法    email -> System.out.println(email));

方法八:用泛型使更加通用化

public static <X, Y> void processElements(    Iterable<X> source,//className instanceof Iterable即可即可    Predicate<X> tester,//className instanceof Predicate即可    Function <X, Y> mapper,//className instanceof Function即可    Consumer<Y> block) {//className instanceof Consumer即可    for (X p : source) {        if (tester.test(p)) {            Y data = mapper.apply(p);            block.accept(data);        }    }}processElements(    roster,    p -> p.getGender() == Person.Sex.MALE        && p.getAge() >= 18        && p.getAge() <= 25,    p -> p.getEmailAddress(),    email -> System.out.println(email));

方法九:使用接受Lambda表达式作为参数的聚合操作

下面的例子是打印在集合roster中符合筛选条件成员的Email:

roster    .stream() //将集合roster转化为对象流    .filter(        p -> p.getGender() == Person.Sex.MALE            && p.getAge() >= 18            && p.getAge() <= 25)    .map(p -> p.getEmailAddress())    .forEach(email -> System.out.println(email));
聚合操作 动作描述 Stream stream() 获取对象流 Stream filter(Predicate predicate) 过滤对象,使之满足Predicate对象 Stream map(Function mapper 通过一个Function对象映射对象成另一个值 void forEach(Consumer action) 通过Consumer对象执行一个操作

Lambda表达式在GUI中的使用

 btn.setOnAction(new EventHandler<ActionEvent>() {            @Override            public void handle(ActionEvent event) {                System.out.println("Hello World!");            }        });//可以简化如下    btn.setOnAction(          event -> System.out.println("Hello World!")        );

Lambda表达式的语法

public class Main {    interface IntegerMath {        int operation(int a, int b);    }    public int operateBinary(int a, int b, IntegerMath op) {        return op.operation(a, b);    }    public static void main(String... args) {        Main myApp = new Main();        IntegerMath addition = (a, b) -> a + b;        IntegerMath subtraction = (a, b) -> a - b;        System.out.println("40 + 2 = " +                myApp.operateBinary(40, 2, addition));        System.out.println("20 - 10 = " +                myApp.operateBinary(20, 10, subtraction));    }}

一个Lambda表达式由以下几部分组成:

  • 如上代码所示,圆括号内由逗号分隔开的参数列表。
  • 箭头符号->。
  • 函数体,它好汉单一的表达式或者块。

方法引用(method references)

用Lambda可创建匿名方法。有时,Lambda表达式仅仅调用一个方法而不做其他的。在这种情况下,通过方法名称引用方法比较清晰明了。方法引用允许你那么做。引用有名称的方法紧凑、易于阅读。

public class Person {    public enum Sex {        MALE, FEMALE    }    String name;    LocalDate birthday;    Sex gender;    String emailAddress;    public int getAge() {        // ...    }    public Calendar getBirthday() {        return birthday;    }        public static int compareByAge(Person a, Person b) {        return a.birthday.compareTo(b.birthday);    }}

假设你的社交网络应用包含一个数组,你想以年龄为依据将它们进行排序,你可以使用以下方法:

Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);class PersonAgeComparator implements Comparator<Person> {    public int compare(Person a, Person b) {        return a.getBirthday().compareTo(b.getBirthday());    }}//排序的签名如下:static <T> void sort(T[] a, Comparator<? super T> c)//所以可以这样使用Lambda表达式,而不需要创建一个实现Comparator的实例Arrays.sort(rosterAsArray,    (Person a, Person b) -> {        return a.getBirthday().compareTo(b.getBirthday());    });

如果两个Person已经存在生日的对比,那么可以用一下Lambda表达式替代:

  public int compareByName(Person a, Person b) {        return a.getName().compareTo(b.getName());    }Arrays.sort(rosterAsArray,    (a, b) -> Person.compareByAge(a, b));

因为上面代码中Lambda仅是调用方法,你可以使用方法引用而不是Lambda表达式:

Arrays.sort(rosterAsArray, Person::compareByAge);

语义上Person::compareByAge等价于(a, b) -> Person.compareByAge(a, b),它们都有以下特征构成:

  • 参数列表的格式是从 Comparator<Person>.compare复制而来,即(Person, Person).

  • 它的主体(body)调用Person.compareByAge.

    方法引用的种类:

种类 例子 引用一个静态方法 ContainingClass::staticMethodName 引用一个对象的方法 containingObject::instanceMethodName 引用一个任意对象的方法 ContainingType::methodName 引用一个构造函数 ClassName::new

例子如下:

“`java
//引用一个静态方法
Person::compareByAge

//引用一个对象的方法
class ComparisonProvider {
public int compareByName(Person a, Person b) {
return a.getName().compareTo(b.getName());
}

  public int compareByAge(Person a, Person b) {      return a.getBirthday().compareTo(b.getBirthday());  }

}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);

//引用一个任意对象的方法
String[] stringArray = { “Barbara”, “James”, “Mary”, “John”,
“Patricia”, “Robert”, “Michael”, “Linda” };
Arrays.sort(stringArray, String::compareToIgnoreCase);

//引用一个构造函数
transferElements(roster, () -> { return new HashSet<>(); });
Set rosterSet = transferElements(roster, HashSet::new);
Set rosterSet = transferElements(roster, HashSet::new);
“`

何时使用嵌套类、局部内部类、匿名内部类以及Lambda表达式

之前提到过,嵌套类可以让你将只在一个地方使用的类进行逻辑地分组,以及增强封装性。局部内部类、匿名内部类以及Lambda表达式也增强了这种优势,但是它们有更加具体化的使用场景:

  • 局部内部类:当你不只实例化类一次,访问类的构造函数或者引入一个新的命名类型时。
  • 匿名内部类:当你需要声明字段或额外的方法。
  • Lambda表达式:当你想把封装成一个单元的行为传递给其他代码时;Use it if you need a simple instance of a functional interface and none of the preceding criteria apply 。
  • 嵌套类:应用场景和局部内部类相似,并且你想使这种类型使用更加广泛,而你又不需要访问局部变量或者方法参数时;当你需要访问外围类的非静态,非共有字段时,使用非静态嵌套类;当你不需要这种权限的时候,使用静态嵌套类。

枚举类型(Enum Types)

枚举类型是一种允许一个变量是一组预先定义常量的其中一个的特殊变量。该变量必须等价于预先定义常量的其中一个。常见的例子包括罗盘方位(NORTH、SOUTH、EAST、and WEST)一周中每天的叫法(星期一等).

因为是常量,所以枚举类型的名称都是大写形式。

在Java编程语言中,用enum关键字定义枚举类。例如:

public enum Day {    SUNDAY, MONDAY, TUESDAY, WEDNESDAY,    THURSDAY, FRIDAY, SATURDAY }

如果你想展示一组固定常量,你应该使用枚举类型。下面是操作Day枚举类型的例子:

public class EnumTest {    Day day;    public EnumTest(Day day) {        this.day = day;    }    public void tellItLikeItIs() {        switch (day) {            case MONDAY:                System.out.println("Mondays are bad.");                break;            case FRIDAY:                System.out.println("Fridays are better.");                break;            case SATURDAY: case SUNDAY:                System.out.println("Weekends are best.");                break;            default:                System.out.println("Midweek days are so-so.");                break;        }    }    public static void main(String[] args) {        EnumTest firstDay = new EnumTest(Day.MONDAY);        firstDay.tellItLikeItIs();        EnumTest thirdDay = new EnumTest(Day.WEDNESDAY);        thirdDay.tellItLikeItIs();        EnumTest fifthDay = new EnumTest(Day.FRIDAY);        fifthDay.tellItLikeItIs();        EnumTest sixthDay = new EnumTest(Day.SATURDAY);        sixthDay.tellItLikeItIs();        EnumTest seventhDay = new EnumTest(Day.SUNDAY);        seventhDay.tellItLikeItIs();    }}The output is:Mondays are bad.Midweek days are so-so.Fridays are better.Weekends are best.Weekends are best.

enum声明定义一个类,称之为枚举类型。枚举类也可以包含方法和其他字段。编译器会在创建枚举类的时候,自动添加一些特殊的方法。例如,静态方法values方法会以enum类声明的顺序返回所有的枚举值。这个方法经常结合for-each循环使用来输出枚举值。如:

for (Planet p : Planet.values()) {    System.out.printf("Your weight on %s is %f%n",                      p, p.surfaceWeight(mass));}

注意:所有的枚举类都会隐含地继承自Java.lang.Enum类。因为Java不支持多继承,所以一个枚举类不能继承其他任意类。


在下面例子中,Planet是一个太阳系中星球的枚举类,它们用mass和radius属性来定义。

Java要求所有的所有的常量都要事先定义,枚举类内的方法或者字段都是以分号结束。


枚举类的构造函数要求是私有或者包私有,因此你不能自己调用枚举类的构造函数。


public enum Planet {    MERCURY (3.303e+23, 2.4397e6),    VENUS   (4.869e+24, 6.0518e6),    EARTH   (5.976e+24, 6.37814e6),    MARS    (6.421e+23, 3.3972e6),    JUPITER (1.9e+27,   7.1492e7),    SATURN  (5.688e+26, 6.0268e7),    URANUS  (8.686e+25, 2.5559e7),    NEPTUNE (1.024e+26, 2.4746e7);    private final double mass;   // in kilograms    private final double radius; // in meters    Planet(double mass, double radius) {        this.mass = mass;        this.radius = radius;    }    private double mass() { return mass; }    private double radius() { return radius; }    // universal gravitational constant  (m3 kg-1 s-2)    public static final double G = 6.67300E-11;    double surfaceGravity() {        return G * mass / (radius * radius);    }    double surfaceWeight(double otherMass) {        return otherMass * surfaceGravity();    }    public static void main(String[] args) {        if (args.length != 1) {            System.err.println("Usage: java Planet <earth_weight>");            System.exit(-1);        }        double earthWeight = Double.parseDouble(args[0]);        double mass = earthWeight/EARTH.surfaceGravity();        for (Planet p : Planet.values())           System.out.printf("Your weight on %s is %f%n",                             p, p.surfaceWeight(mass));    }}If you run Planet.class from the command line with an argument of 175, you get this output:$ java Planet 175Your weight on MERCURY is 66.107583Your weight on VENUS is 158.374842Your weight on EARTH is 175.000000Your weight on MARS is 66.279007Your weight on JUPITER is 442.847567Your weight on SATURN is 186.552719Your weight on URANUS is 158.397260Your weight on NEPTUNE is 199.207413

参考来源
Java SE官网文档