本帖最后由 ufof 于 2016-5-5 00:43 编辑



8.1 成员内部类



8.1.1 定义成员内部类

我们的生活中是由对象构成的。然而一类对象也可以分解成不同的几类对象。例如人类中包含脑类、心脏类、胃类等。在Java当中,内部类机制就是代表“某类的一部分”所产生的。

内部类可以轻而易举的访问外部类的成员。

内部类也算是外部类一个成员之一。

  1. class InnerClassDemo {
  2.     public static void main(String[] args) {

  3.     }
  4. }

  5. class Outer{         //外部类
  6.     class Inner{     //内部类
  7.         
  8.     }
  9. }
复制代码

从语法方面,直接在外部类中嵌套一个类即可。

8.1.2 内部类的实例化

内部类的实例化语法如下:

  1. 外部类.内部类 实例名 = 外部类对象.内部类对象;
复制代码

在我们这个例子当中,应当是这样实例化:
  1. Outer.Inner i = new Outer().new Inner();
复制代码

我们写一个例子:
  1. class InnerClassDemo {

  2.     public static void main(String[] args) {
  3.         Outer.Inner i = new Outer().new Inner();    //实例化内部类
  4.         i.print();                                    //调用内部类print方法
  5.     }
  6. }

  7. class Outer{
  8.         
  9.     class Inner{
  10.         void print(){
  11.             System.out.println("hello");
  12.         }
  13.     }
  14. }
复制代码
结果:



在这个例子当中,我在Inner类中定义了print()方法。如果要想访问这个方法,需要实例化。所以我在主方法中,通过实例化语法实例化了Inner类,然后通过实例调用print()方法,即可。

8.1.3 内部类的访问规则

内部类可以很方便的访问外部类中的成员,即使它被声明为private。

  1. class InnerClassDemo2 {

  2.     public static void main(String[] args) {
  3.         Outer.Inner i = new Outer().new Inner();
  4.         i.printNum();
  5.     }
  6. }

  7. class Outer{
  8.     private int num = 10;                //定义私有变量num
  9.     class Inner{                        //内部类
  10.         void printNum(){
  11.             System.out.println(num);
  12.         }
  13.     }
  14. }
复制代码
结果:


在这个程序当中,我在Outer类当中定义了私有int类型字段。在内部类中直接打印输出,没有任何问题。

如果内部类方法中的变量和内部类的字段冲突,优先使用局部变量。

  1. class InnerClassDemo2 {

  2.     public static void main(String[] args) {
  3.         Outer.Inner i = new Outer().new Inner();
  4.         i.printNum();
  5.     }
  6. }

  7. class Outer{
  8.     class Inner{                        //内部类
  9.         int num = 1;                    //定义内部类字段num
  10.         void printNum(){
  11.             int num = 2;                //定义方法中的局部变量num
  12.             System.out.println(num);
  13.         }
  14.     }
  15. }
复制代码
结果:


那么,要是我想要调用内部类的字段而非局部变量呢?用this!

  1. class InnerClassDemo2 {

  2.     public static void main(String[] args) {
  3.         Outer.Inner i = new Outer().new Inner();
  4.         i.printNum();
  5.     }
  6. }

  7. class Outer{
  8.     class Inner{                        //内部类
  9.         int num = 1;                    //定义内部类字段num
  10.         void printNum(){
  11.             int num = 2;                //定义方法中的局部变量num
  12.             System.out.println(this.num);
  13.         }
  14.     }
  15. }
复制代码
结果:


可以看得出来,这次打印出来了内部类的字段,而不是局部变量。

现在最大的问题是:外部类字段和内部类字段冲突了怎么办?

如果只通过this,调用的肯定是内部类的字段。但是如果我想要调用外部类的字段,需要注明是谁的this。语法是:
  1. 外部类.this.字段;
复制代码
我们应用一下:
  1. class InnerClassDemo2 {

  2.     public static void main(String[] args) {
  3.         Outer.Inner i = new Outer().new Inner();
  4.         i.printNum();
  5.     }
  6. }

  7. class Outer{
  8.     int num = 1;
  9.     class Inner{                        //内部类
  10.         int num = 2;                    //定义内部类字段num
  11.         void printNum(){
  12.             System.out.println(Outer.this.num);
  13.         }
  14.     }
  15. }
复制代码
结果:


现在,我们应该知道了如何调用外部类的成员了。

本章小结:
  • 内部类机制用于表示某个类的一部分
  • 内部类的实例化语法为“外部类.内部类 实例名 =  外部类对象.内部类对象”
  • 如果要调用外部类的字段,需要注明是谁的this



8.2 局部内部类



8.2.1 局部内部类的定义

我们之前定义的成员内部类,这种内部类是与其他的成员同级的。如果一个内部类,它只需要在一个方法中使用,可以在方法中定义一个内部类。这种内部类叫做局部内部类。

  1. class Outer{            //外部类
  2.     void outerMethod(){    //方法
  3.         class Inner{    //局部内部类
  4.             
  5.         }
  6.     }
  7. }
复制代码

这个类当中也可以定义字段和方法。

  1. class Outer{                            //外部类
  2.     void outerMethod(){            //方法
  3.         class Inner{                    //局部内部类
  4.             void innerMethod(){
  5.                 System.out.println("innerMethod() runs");
  6.             }
  7.         }
  8.     }
  9. }
复制代码

我在Inner类中定义innerMethod()。我该如何在outerMethod()中调用呢?
应该在声明局部内部类之后新建这个内部类的对象,通过实例调用。
由于只需要调用一次,可以使用匿名对象的方式。

  1. class Outer{                            //外部类
  2.     void outerMethod(){                    //方法
  3.         class Inner{                    //局部内部类
  4.             void innerMethod(){
  5.                 System.out.println("innerMethod() runs");
  6.             }
  7.         }

  8.         new Inner().innerMethod();        //匿名对象调用innerMethod()
  9.     }
  10. }
复制代码
大家一定要注意,要在定义类之后才能新建对象。如果都没有读到哪里来的对象?

我们在主方法中应该做的事情是新建Outer对象,调用outerMethod()即可。

  1. class LocalInnerClassDemo {
  2.     public static void main(String[] args) {
  3.         new Outer().outerMethod();
  4.     }
  5. }

  6. class Outer{                            //外部类
  7.     void outerMethod(){                    //方法
  8.         class Inner{                    //局部内部类
  9.             void innerMethod(){
  10.                 System.out.println("innerMethod() runs");
  11.             }
  12.         }
  13.         new Inner().innerMethod();        //匿名对象调用innerMethod()
  14.     }
  15. }
复制代码
结果:


局部内部类的访问规则和成员内部类差不多,这里就不在赘述了。但是有一点:

在JDK7或以下,局部内部类要是想要访问其所在的方法的数据,其必须是final。这是因为其所在的方法结束之后,其中的数据也随之消失,但是内部类仍然在堆内存中。被声明为final的数据有特殊的储存方法,所以说其不会消失。

但是在JDK8,这个问题被修复了。字段不需要被显式的声明为final,但是其不能被重新赋值。

本章小结
  • 如果一个内部类只需要被一个方法调用,可以在这个方法里面定义内部类
  • 如果要调用内部类的方法/字段,需要在其所在的方法中实例化对象,才可以使用



8.3 匿名内部类



8.3.1  概念引入

请观察下列代码:

  1. class AnonymousInnerClassDemo{
  2.    
  3.     public static void main(String[] args) {
  4.         
  5.     }

  6.     public static void method(MyInterface myInterface) {
  7.         System.out.println("method() runs");
  8.     }
  9. }

  10. interface MyInterface{
  11.     void function();
  12. }
复制代码

在这个程序当中,method()方法接收一个MyInterface对象的实例。我们都知道,接口不能被实例化,但是根据接口多态,我们可以传入一个MyInterface接口的实现类的实例。因此,我们需要写一个实现MyInterface的类,将这个类实例化,然后再把这个实例传入method()中。

  1. class AnonymousInnerClassDemo{
  2.    
  3.     public static void main(String[] args) {
  4.         ImplementingClass ic = new ImplementingClass();    //实例化这个实现类
  5.         method(ic);                                                           //传入这个实例
  6.     }

  7.     public static void method(MyInterface myInterface) {
  8.         System.out.println("method() runs");
  9.     }
  10. }

  11. class ImplementingClass implements MyInterface {    //MyInterface实现类
  12.     public void function() {                                //复写function()方法
  13.         System.out.println("function() runs");
  14.     }
  15. }


  16. interface MyInterface{
  17.     void function();
  18. }
复制代码
结果:



很容易发现,就为了调用一个方法,就要创建一个类。很麻烦。匿名内部类就可以在此使用。

8.3.2 匿名内部类

匿名内部类是一个“没有名字”的类的对象。通过这个对象我们可以简化上述代码。若满足下列两项,可以使用匿名内部类进行化简:

  • 该类必须实现一个接口或继承一个类
  • 该类只需要使用一次

匿名内部类定义方法如下:

  1. new 实现的接口|父类(构造方法参数){
  2.     //在这里面复写接口或父类的抽象方法
  3. }
复制代码

整个这一片代码都是一个对象。
我们可以通过这个形式,将method()中传入的代码简化成:

  1. method(new MyInterface(){
  2.     public void function() {
  3.         System.out.println("function() runs");
  4.     }
  5. });
复制代码
结果同上。

我这个没有名字的类,实现了MyInterface接口。因此我要new的是MyInterface。new MyInterface()之后的大括号中,就要复写MyInterface的抽象方法,也就是function()。因此,我们不需要专门写一个类去实现。可以发现这种形式简化了代码。

本章小结
  • 如果需要使用一个实现某个接口的,或继承某个类的类的实例,而且只使用一次,可以使用匿名内部类简化
  • 匿名内部类是一个没有名字的类的对象
  • 匿名内部类的语法是:“new 接口|父类(){}”,大括号中复写接口/父类的抽象方法



8.4 Object类



8.4.1 Object类概述

Object类是十分特殊的一个类。它处于继承树的最上端。任何类都以Object类作为父类,相当于“上帝”。所以说:

  1. class AnyClass{}
复制代码
等价于:
  1. class AnyClass extends Object{}
复制代码

因此,所有类都有Object类中定义的方法。

8.4.2 equals()方法

equals()方法接收Object类对象(因此,根据多态,他什么对象都能接收),返回布尔类型。如果调用equals()方法的实例和参数中的对象在内存中的地址一样,返回true。

我们在讲堆内存的时候,曾提到了实例指向对象。每一个对象都有自己的唯一的地址,就像是我们的身份证号。

  1. class ObjectDemo {
  2.     public static void main(String[] args) {
  3.         MyClass mc1 = new MyClass();
  4.         MyClass mc2 = new MyClass();

  5.         System.out.println(mc1.equals(mc2));
  6.     }
  7. }

  8. class MyClass{}
复制代码
结果:



这是为什么呢?我们在new一个对象时,其会被分配一个地址,然而地址是唯一的,两个对象不可能共享一个地址。

  1. class ObjectDemo {
  2.     public static void main(String[] args) {
  3.         MyClass mc1 = new MyClass();
  4.         MyClass mc2 = mc1;

  5.         System.out.println(mc1.equals(mc2));
  6.     }
  7. }

  8. class MyClass{}
复制代码
结果:



请大家看第四行。mc2这个实例,我并没有给他new一个对象赋给他,而是将之前的mc赋给了他。因此,mc和mc2都指向同一个地址。所以返回true。



这个方法经常被子类复写。例如String类中的equals()方法就复写了这个方法。

8.4.3 hashCode()

hashCode()方法不接收任何参数,返回int类型。其返回的是调用这个方法的实例的地址值(十进制)。其实equals()方法就是在比较两个实例的hashCode()是否相等。

  1. class ObjectDemo {
  2.     public static void main(String[] args) {
  3.         MyClass mc1 = new MyClass();
  4.         MyClass mc2 = mc1;

  5.         System.out.println(mc1.hashCode());
  6.         System.out.println(mc2.hashCode());
  7.     }
  8. }

  9. class MyClass{}
复制代码
结果:



可以看出来,刚才equals()返回真的两个实例的hashCode值是相等的。

8.4.4 toString()方法

toString()方法不接收参数,返回字符串类型。这个方法通过字符串的表现形式,以说明这个实例。

  1. class ObjectDemo {
  2.     public static void main(String[] args) {
  3.         MyClass mc = new MyClass();
  4.         System.out.println(mc.toString());
  5.     }
  6. }

  7. class MyClass{}
复制代码
结果:



那么,这个字符串究竟什么意思呢?
首先,我们看'@'前面,MyClass是实例的类。'@'后面是实例的十六进制地址值(我们之前的hashCode()返回的是10进制)。

关于这个方法,还有一个有趣的知识点:若直接打印一个类的实例,实际上打印的是这个实例的toString()方法。如果你想要自定义打印一个实例的结果,你可以把这个实例所在的类的toString()方法复写。

  1. class ObjectDemo {

  2.     public static void main(String[] args) {
  3.         MyClass mc = new MyClass();
  4.         System.out.println(mc);        //打印MyClass类的实例
  5.     }
  6. }

  7. class MyClass{

  8.     public String toString(){    //复写Object类的toString()方法
  9.         return "哈哈哈";
  10.     }
  11. }
复制代码
结果:




本章小结
  • Object类是所有类的父类,如果一个类不显式的extends其他类,他默认继承Object类
  • Object类的equals()方法用于判断两个实例所指向的对象的地址是否相同
  • hashCode()返回实例的对象的地址值(十进制)
  • toString()返回:类名+@+十六进制地址



8.5 单例设计模式



8.5.1 设计模式概述

设计模式是指在实际开发当中经常使用到的、重复的、人人皆知的代码经验。目前来讲,一共有23种设计模式。单例设计模式是其中最容易理解的一种。

8.5.2 单例设计模式概述

单例设计模式应用于类当中。被应用的类叫做单例类。单例类只能有一个对象。单例类在实际开发中十分常见,许多的类只允许有一个对象。所以说这个设计模式是必须得要掌握的。

8.5.3 饿汉式

常见的单例设计模式有两种方法:饿汉式和懒汉式。 饿汗式是比较容易理解的。

首先我们需要把单例类的唯一的构造方法声明为private,这样这个类不能被new出来。

  1. class SingletonClass{
  2.     private SingletonClass(){}   //private构造方法
  3. }
复制代码
现在,我们需要在类的成员位置上写一个字段,是这个类的实例。这个实例必须被private修饰,因为我们不希望用户访问它;需要被声明为static,毕竟这个字段属于类;最后还需要声明为final,因为它不需要改变。

  1. class SingletonClass{
  2.     private static final SingletonClass instance = new SingletonClass();
  3.     private SingletonClass(){}
  4. }
复制代码

那么,现在我们需要定义一个方法,用于返回instance。要注意,这个方法必须为static,毕竟我们没有实例。

  1. class SingletonClass{
  2.     private static final SingletonClass instance = new SingletonClass();
  3.     private SingletonClass(){}
  4.    
  5.     public static SingletonClass getInstance(){
  6.         return instance;
  7.     }
  8. }
复制代码
好的,我们写一个主类验证一下:

  1. class SingletonClass{
  2.     private static final SingletonClass instance = new SingletonClass();
  3.     private SingletonClass(){}
  4.    
  5.     public static SingletonClass getInstance(){
  6.         return instance;
  7.     }
  8. }

  9. class SingletonDemo{
  10.     public static void main(String[] args){
  11.         SingletonClass sc1 = SingletonClass.getInstance();
  12.         SingletonClass sc2 = SingletonClass.getInstance();
  13.         
  14.         System.out.println(sc1.equals(sc2));
  15.     }
  16. }
复制代码
结果:



在主方法中的两个实例中,我都调用了SingletonClass的getInstance()方法。最后输出sc1.equals(sc2),也就是比较两者的地址值是否相等。结果为true,这证明堆内存中的确只有一个SingletonClass的对象。

那么,这个是什么原理呢?很简单,既然实例被声明为final,其永不改变。getInstance()方法只将其返回,当然是一个对象。

8.5.4 懒汉式

懒汉式是另一种做法。首先,构造方法要private,这点不多说。但是区别在于字段的实例:

  1. class SingletonClass{
  2.     private static SingletonClass instance = null;
  3.     private SingletonClass(){}
  4.    
  5. }
复制代码
注意:这个字段不能是final,不然它就终身是空了!

在getInstance()方法中也有一些改变:

  1. class SingletonClass{
  2.     private static SingletonClass instance = null;
  3.     private SingletonClass(){}
  4.    
  5.     public static SingletonClass getInstance(){
  6.         if(instance == null){
  7.             instance = new SingletonClass();
  8.             return instance;
  9.         }
  10.         else{
  11.             return instance;
  12.         }
  13.     }
  14. }
复制代码
这个方法的逻辑是:如果instance仍为空(那么你就是第一次调用getInstance()方法),我就给你初始化,然后返回;如果不是空(可见你已经调用过了这个方法),不在初始化,直接返回。

添加主方法验证:

  1. class SingletonClass{
  2.     private static SingletonClass instance = null;
  3.     private SingletonClass(){}
  4.    
  5.     public static SingletonClass getInstance(){
  6.         if(instance == null){
  7.             instance = new SingletonClass();
  8.             return instance;
  9.         }
  10.         else{
  11.             return instance;
  12.         }
  13.     }
  14. }

  15. class SingletonDemo{
  16.     public static void main(String[] args){
  17.         SingletonClass sc = SingletonClass.getInstance();
  18.         SingletonClass sc2 = SingletonClass.getInstance();
  19.         
  20.         System.out.println(sc.equals(sc2));
  21.     }
  22. }
复制代码
结果:



本章小结
  • 设计模式是常用的代码经验
  • 单例设计模式是让某各类(成为单例类)只能有一个对象
  • 单例设计模式有两个写法:饿汉式、懒汉式



8.6 包



8.6.1  包概述

一个项目是由成千上万个类组成的,这一点不难理解。但是随之就会带来一些烦恼:类不能重名,所以说给类命名就变得麻烦。为了解决这个问题,Java为我们提供了包机制。简单的来说,包为整个项目提供层次。某些类是干某些事情的,就放在这个包中;某些类是干别的事情的,就放在另一个包中。这样不仅仅解决了命名问题,还给程序增加了层次感。可见,对包的理解是十分重要的。

8.6.2  用package声明包

package用于声明包。其必须出现在代码的第一行。紧随其后的是包名。

  1. package mypackage;        //声明mypackage包

  2. class PackageDemo{
  3.     public static void main(String[] args){
  4.         System.out.println("hello, world");
  5.     }
  6. }
复制代码
这段程序编译时没有问题,可是在运行时除了一些小插曲:



我明明有PackageDemo.class文件啊?为什么提示找不到?

这是因为,包是以文件夹的形式体现出来的。由于程序第一行明确标示了“package mypackage”,其会在命令提示符所在的路径下找mypackage目录,如果找不到文件夹,自然会报错。

那么,我们需要在目前的目录下新建一个文件夹,叫做mypackage,把.class文件放进来。




现在,我们再编译一下。(注意:命令行的路径不需要改变!)



依旧是报错..怎么回事?
我们之前提过了,包用于将类分成几大部分。如果不同的包有同名的类,系统怎么知道你指的是哪个类?所以说,在Java中,类的全名是“包名.类名”。在运行时必须要声明这个是mypackage下的类,就可以通过。




在类名前面多加一个“mypackage.”就解决了这个问题。程序顺利运行。

8.6.3 让系统自动帮你创建文件夹

在上面的演示当中,我们是手动创建文件夹作为包的。但是,有一种更好的办法,可以让程序在编译时就帮你创建好文件夹。这只需要在编译(javac)时附加一点参数即可。

  1. javac -d 路径 源文件.java
复制代码

-d是javac的附加参数之一。它用于指定放置生成的类文件的位置。后面需要跟上一个路径,这个路径便是创建文件夹的位置。

如果路径是本路径,可以用"."代替。



通过这样的方式编译源文件,便可以在指定的路径下自动创建文件夹。

8.6.4 import关键字

通过包对类进行区分后,会产生一个问题:一个类想要访问另一个包中的另一个类怎么办?

我曾经提到过,类名的全称是“包名.类名”。所以说如果要访问别的包的类,需要声明其的包名。

假如说,我在mypackage包下有一个ImportDemo类,在mypackage2包下有一个Dog类,如果需要让ImportDemo类访问Dog类,需要这样写:


  1. package mypackage;        //声明mypackage包

  2. class PackageDemo{
  3.     public static void main(String[] args){
  4.         mypackage2.Dog myDog = new mypackage2.Dog();   //在访问Dog类时需要说明其是mypackage2包的
  5.         myDog.bark();
  6.     }
  7. }
复制代码
结果:



可见,对其他的包的类访问需要在类前面加上他的包名。这样做十分麻烦。为了解决这个问题,Java为我们提供了import关键字。import关键字一般写在package之后。格式为“import 包名.类名”。当一个类被import之后,在这个类中访问被import的类不再需要声明包名。

注意:由于Dog类在其它的包中,它必须被声明为public才能被本包访问!

  1. <font size="3">package mypackage;        //声明mypackage包
  2. import mypackage2.Dog;  //导入mypackage2包下的Dog类

  3. class PackageDemo{
  4.     public static void main(String[] args){
  5.         Dog myDog = new Dog();
  6.         myDog.bark();
  7.     }
  8. }</font>
复制代码
结果:



如果我的类需要用到另一个包中的大部分类,并不需要逐个import。可以使用星号“*”来代表该包下的所有类。

  1. <font size="3">package mypackage;        //声明mypackage包
  2. import mypackage2.*;     //导入mypackage2包下的所有类

  3. class PackageDemo{
  4.     public static void main(String[] args){
  5.         mypackage2.Dog myDog = new mypackage2.Dog();
  6.         myDog.bark();
  7.     }
  8. }</font>
复制代码
结果:



注意:使用星号“*”仅仅会导入包中的所有类,但是并不会导入指定的包的子包的类。如果要访问这个包的子包,需要import这个包的子包,格式为“import 父包.子包.类”。

学生提问:我怎么感觉我没有import过java.lang包都能使用它的类?

回答:java.lang包中是Java中最基础、最重要的类。为了方便操作,每一个.java文件自动导入java.lang包,不需要程序员手动导入。

8.6.5 包的命名规范

类的重名问题是解决了,但是包难道不会重名吗?因此,Oracle公司推荐使用拥有的域名倒过来写来避免这个问题。例如我拥有网站“www.abcdef.com”,我可以把包命名为“com.abcdef.www.(自定义名)”。如果我拥有邮箱“abcd@efg.com”,我的包名可以是“com.efg.abcd.(自定义名)”。其中自定义名是自己取的,一般是该包中的类的一个概述。最后,要注意所有的字母为小写。

8.6.6 import static

import static是JDK 1.5增加的新功能。这个功能可以很大的简化代码量。
import static用于导入类中所有的静态成员。一旦静态成员被导入,本类不需要再通过“类名.成员”来调用,只需要写成员名就行了。

import static语法如下:
  1. import static 包名.类名.静态成员;
复制代码
如果想要导入一个类的所有静态成员,静态成员可以用星号“*”代替。

这里我们举一个例子。我们之前一直在写的System.out.println(),其实是这样的:System是类,out是PrintStream(标准输出流类)的静态实例,println()便是PrintStream类的方法。

我们静态导入out:

  1. import static java.lang.System.*;  //静态导入System类中的所有静态成员

  2. class ImportStaticDemo{
  3.     public static void main(String[] args){
  4.         out.println("hello");    //由于out是System的静态字段,可以不加上类名
  5.     }
  6. }
复制代码
结果:




本章小结
  • 包用于解决类重名问题
  • 包以文件夹的形式存在
  • 通过“package 包名;”声明类所在的包,这段代码需要出现在代码的第一行
  • 在通过package声明之后,还需要把这个类放在跟包名同名的文件夹下
  • 可以通过“javac -d 路径  源文件名”来让系统帮你自动创建文件夹
  • 运行时需要声明类的包名,“java  包名.类名”
  • 如果一个类要访问另一个包中的其他类,需要通过以下格式:“包名.类名”
  • 通过import关键字,可以简化上述内容,直接通过类名来访问,格式为“import 包名.类名”
  • 如果想要导入一个包中的所有类,可以通过星号“*”表示,格式为“import  包名.*”
  • 通过上述方式导入包只会导入该包中的所有类,但不会导入该包的子包的类
  • 类的命名规范为域名倒过来写,全部字母小写
  • import static是Java 1.5增加的功能,用于导入指定类的静态成员
  • 格式为“import static 包名.类名.静态成员”,如果想要导入所有,可以使用星号代替
  • 当一个静态成员被导入,该类中默认拥有该成员,不再需要通过“类名.成员”来调用




8.7  枚举



8.7.1  枚举概述

某一类事物是只有限定的某些值的。例如只有春夏秋冬四个季节、一个星期只有七天、性别只有男女..等等。在实际开发当中,这种只有限定的值的类型是十分常用的。在比较早的时候,只可以通过已有的值来代替这种值。例如四个季节可以用整数类型1,2,3,4表示;性别可以用布尔表示,true表示男、false表示女。这样做有这几种缺点:

  • 安全隐患。假如说用1,2,3,4表示春夏秋冬,万一我给一个5怎么办?
  • 没有意义。如果我打印输出“春”,实际输出的结果是1,看不出来他表示的是春天。

为此,Java 5新增一个关键字:enum。enum是一个特殊的类,叫做枚举类。和class、interface是相同的地位。现在我们来学习如何使用enum吧。

8.7.2 定义和使用enum

enum定义语法如下:

  1. enum 枚举类名{
  2. }
复制代码

定义这种枚举类的值的语法如下:

  1. enum 枚举类名{
  2.     值1,值2,值3,....值n;
  3. }
复制代码
现在,我们来定义一个枚举类。这个枚举类有星期一、星期二、星期三......星期天这七个值。

  1. enum WeeksInDay{
  2.     MONDAY,TUESDAY,THURSDAY,WEDNESDAY,FRIDAY,SATURDAY,SUNDAY;
  3. }
复制代码
注意:由于enum中的值都是最终类型(final),应当使用常亮的命名方式。也就是所有字母大写,用下划线“_”代替空格。

那么,我们该如何使用这个枚举类呢?
枚举类类型定义语法:

  1. 枚举类 实例名 = 枚举类.枚举值;
复制代码

例子:

  1. enum WeeksInDay{
  2.     MONDAY,TUESDAY,THURSDAY,WEDNESDAY,FRIDAY,SATURDAY,SUNDAY;
  3. }

  4. class EnumDemo{
  5.     public static void main(String[] args){
  6.         WeeksInDay day = WeeksInDay.FRIDAY;   //WeeksInDay类型
  7.     }
  8. }
复制代码
如果我们打印day引用(其实直接打印WeeksInDay.FRIDAY也行):

  1. enum WeeksInDay{
  2.     MONDAY,TUESDAY,THURSDAY,WEDNESDAY,FRIDAY,SATURDAY,SUNDAY;
  3. }

  4. class EnumDemo{
  5.     public static void main(String[] args){
  6.         WeeksInDay day = WeeksInDay.FRIDAY;
  7.         System.out.println(day);   //打印day
  8.     }
  9. }
复制代码
结果:



8.7.3 枚举类中的一些方法

枚举类也是类,但是它没有继承Object类,而是继承了Enum类。Enum类中有两个常用的方法
  • String toString()
  • int ordinal()

在我们打印输出一个枚举的值时,其实打印的是其调用toString()的返回值。所以说toString()以字符串形式返回其在枚举类中的值。

  1. enum WeeksInDay{
  2.     MONDAY,TUESDAY,THURSDAY,WEDNESDAY,FRIDAY,SATURDAY,SUNDAY;
  3. }

  4. class EnumDemo{
  5.     public static void main(String[] args){
  6.         WeeksInDay day = WeeksInDay.FRIDAY;
  7.         String str = day.toString();   //将day实例的toString()方法的返回值赋给String类的实例str
  8.         
  9.         System.out.println(str);   //打印str
  10.     }
  11. }
复制代码
结果:



ordinal()方法返回的是实例对应的值的序号。枚举中的每一个值都有对应的序号,从0开始排列,与数组的下标一个道理。

  1. enum WeeksInDay{
  2.     MONDAY,TUESDAY,THURSDAY,WEDNESDAY,FRIDAY,SATURDAY,SUNDAY;
  3. }

  4. class EnumDemo{
  5.     public static void main(String[] args){
  6.         WeeksInDay day = WeeksInDay.FRIDAY;
  7.         
  8.         System.out.println(day);
  9.         System.out.println(day.ordinal());        //打印day对应的序号
  10.     }
  11. }
复制代码
结果:



由于FRIDAY是WeeksInDay的第五个值,其的序号是4(毕竟从0开始数)。

8.7.4 接收枚举的switch

在第三章,我们学习过多分支语句switch。switch除了接收一些基本数据类型外,还接收枚举类。其通过枚举类的值执行对应的语句。语法我就不说了,见第三章。

  1. enum WeeksInDay{
  2.     MONDAY,TUESDAY,THURSDAY,WEDNESDAY,FRIDAY,SATURDAY,SUNDAY;
  3. }

  4. class EnumDemo{
  5.     public static void main(String[] args){        
  6.         WeeksInDay day = WeeksInDay.TUESDAY;
  7.         
  8.         switch(day){
  9.         case MONDAY:
  10.             System.out.println("今天星期一");
  11.             break;
  12.         case TUESDAY:
  13.             System.out.println("今天星期二");
  14.             break;
  15.         case THURSDAY:
  16.             System.out.println("今天星期三");
  17.             break;
  18.         case WEDNESDAY:
  19.             System.out.println("今天星期四");
  20.             break;
  21.         case FRIDAY:
  22.             System.out.println("今天星期五");
  23.             break;
  24.         case SATURDAY:
  25.             System.out.println("今天星期六");
  26.             break;
  27.         case SUNDAY:
  28.             System.out.println("今天星期天");
  29.             break;
  30.         }
  31.     }
  32. }
复制代码
结果:



本章小结
  • 枚举类用于表现只有特定的值的数据类型
  • 枚举类对应的关键字是enum
  • 在enum当中定义枚举类的可能出现的值
  • Enum类的toString()方法返回以字符串表现形式的枚举值
  • ordinal()返回枚举值的序号,这个序号从0开始计算
  • switch可以接收枚举类型



8.8 权限修饰符:default和protected



8.8.1 四个权限修饰符之间的关系

在面向对象(上)的章节中,我讲到过public和private这两个权限修饰符。它们分别表示公有和私有。被声明为public的成员可以在全局范围内被访问;反之,被private声明的成员只能在本类中被访问。由于default和protected涉及到包和继承这两个概念,当时我没有讲。

这四个权限修饰符的关系如下:



那么,中间两个修饰符到底是如何限定权限的呢?请往下看。

8.8.2 default

default修饰符并不能显式的修饰。只不过没有指定任何权限修饰符的成员就默认是default罢了。
被声明为default的成员仅在本包中可以被访问。

假如说在mypackage包下有一个PermissionDemo主类,在mypackage2包下有Test类。Test类如此定义:

  1. package mypackage2;

  2. public class Test{
  3.     int x;            //default权限字段
  4.     public int y;    //public权限字段
  5. }
复制代码

现在,我们在mypackage包下的PermissionDemo类中实例化Test类,访问这两个字段:

  1. package mypackage;

  2. import mypackage2.Test;

  3. class PermissionDemo{
  4.     public static void main(String[] args){
  5.         Test t = new Test();
  6.         t.y = 10;   
  7.         t.x = 5;     //非法
  8.     }
  9. }
复制代码
结果:



由于PermissionDemo类在Test类在不同的包中,PermissionDemo无权访问default的字段x。因此发生编译时错误。如果我们将x声明为public,这个问题就能得到解决。

8.8.3 protected

protected从权限上来讲,仅仅比default多一点点。protected也是同包内可以访问,区别在于,不同包的子类可以继承protected成员

假设我们把刚才的Test类改成这样:

  1. package mypackage2;

  2. public class Test{
  3.     protected static int x;            //protected权限字段
  4. }
复制代码

PermissionDemo改成:

  1. package mypackage;

  2. import mypackage2.Test;

  3. class PermissionDemo extends Test{   //继承Test类
  4.     public static void main(String[] args){
  5.         x = 5;                                     //对继承过来的字段赋值
  6.         System.out.println(x);
  7.     }
  8. }
复制代码
结果:

'

可以看得出来,Test中的字段被成功的继承过来了。

如果我们把Test类中的x改成private或者default,会报出编译时错误:



可见,一般来讲被protected声明的成员都是让子类去用的。如果是不同包,但是不是子类,就用不了了。
最后再用一张表格总结一下四个权限修饰符:

权限修饰符
private default protected public
本类

本包
×
不同包子类
× ×
全局范围内
× × ×


本章小结
  • 权限修饰符权限从小到大依次为:“private、default、protected、public”
  • default的成员只能在本包中被访问,而且不能被显式的修饰
  • protected除了只能在本包中被访问外,其他包的子类可以继承



8.9 javadoc



8.9.1 javadoc概述

如果我们编写了一个程序,需要让用户去使用,用户必须要知道这个程序具体能干什么、怎么用。为了解决这个问题,我们必须给程序写一个对应的说明书。当然了,要使用Microsoft Word或类似软件写说明书当然也可以。不过sun公司为我们提供一项技术:javadoc。只需要通过几个命令,就可以生成程序的说明书。这种说明书的排版、层次安排的也十分好,推荐大家使用。

javadoc生成的是多个html文件。这类给程序进行对外暴露的说明叫做“应用程序编程接口”,英文Application Programming Interface,简称API。至于这种帮助文档是什么外观的,可以参考Java SE 8官方API:这里

8.9.2 生成javadoc

为了方便演示,我先定义这样的一个类:

  1. public class JavaDocDemo{
  2.     //字段
  3.     public int x;
  4.     private String str;
  5.     int y;
  6.     //方法
  7.     public void method(){}
  8.     protected static int method2(){ return 0;}
  9.     private String method3(){return "hello";}
  10.     //构造方法
  11.     public JavaDocDemo(){}
  12.     public JavaDocDemo(int x, int y){}
  13. }
复制代码

生成javadoc的基本命令如下:

  1. javadoc 源文件名
复制代码

如果想要在指定的目录下生成javadoc,命令如下:

  1. javadoc -d 目录 源文件名
复制代码

如果类不在任何包下,不需要输入包名。
注意:要被生成javadoc的类必须声明为public!

好的,现在我们通过第一个命令,生成一下刚才我们定义的类的帮助文档。



在键入命令之后,命令行会显示出创建时的信息。如果没有报出错误,javadoc帮助文档应当正确的被生成。



index.html是所有其他的html文件的集合。我们用自己的浏览器打开看一下:



方法特写:



字段特写:



构造方法特写:



不难发现:只有某些成员被显示出来了,有一些没有。这是为什么?

在javadoc中,只有被声明为public或protected的成员才会被显示


点开任意方法/构造方法的超链接,可以查看详细信息:



不过可以发现,任何一个方法都没有一个详细的说明。这是因为我们没有写。那么具体是在哪里写呢?这里涉及到我们在第一章中提到的:文档注释。

8.9.3 文档注释以及标签

文档注释一般放在类和类的成员之前。用于给这个成员加上一个详细的描述。格式如下:

  1. /**
  2. 描述
  3. */
复制代码

和多行注释的区别是:多行注释开头只有一个星号*,文档注释有两个。

我们现在给类和类中的成员都添加一个文本注释:

  1. /**
  2. 这是一个用于演示javadoc的类
  3. */
  4. public class JavaDocDemo{
  5.     /**
  6.     这是一个int类型字段x
  7.     */
  8.     public int x;
  9.    
  10.     /**
  11.     这是一个String类型字段str
  12.     */
  13.     private String str;
  14.     int y;
  15.    
  16.     /**
  17.     这是一个公共、无返回值的方法method()
  18.     */
  19.     public void method(){}
  20.    
  21.     /**
  22.     这是一个protected、静态、返回int的方法method2()
  23.     */
  24.     protected static int method2(){ return 0;}
  25.     private String method3(){return "hello";}
  26.    
  27.     /**
  28.     这是一个无参的构造方法
  29.     */
  30.     public JavaDocDemo(){}
  31.    
  32.     /**
  33.     这是一个接收两个int类型的构造方法
  34.     */
  35.     public JavaDocDemo(int x, int y){}
  36. }
复制代码

好的,现在我们给类中的成员都加上了其对应的文档注释(这次会有一些警告,是因为没有加上标签,不过现在不需要理会)。重新生成javadoc,再来看看:



方法摘要以及字段摘要:



那么,现在我们已经给类以及类的成员都添加了一个描述。但是一个方法/构造方法是有可能有参数以及返回值的,我们也需要对这些参数和返回值进行特别的描述。为了增强排版,可以使用标签来对这几个参数/返回值进行描述。

参数的标签为“@param  参数名 描述”,返回值的标签为“@return 描述”。这几个标签需要放在文档注释中。

  1.     /**
  2.     这是一个protected、静态、返回int的方法method2()
  3.     @param a 这是一个int类型参数
  4.     @param str 这是一个String类型参数
  5.     @return 这个方法会返回0
  6.     */
  7.     protected static int method2(int a, String str){ return 0;}
  8.    
  9.     /**
  10.     这是一个接收两个int类型的构造方法
  11.     @param x 这是一个int类型参数
  12.     @param y 这是另一个int类型参数
  13.     */
  14.     public JavaDocDemo(int x, int y){}
复制代码
重新生成javadoc,会看到:



以及:



可以发现,参数以及返回值可以通过文档注释中的标签来给予一个描述。

常用的标签还有两个:“@version 版本号”,还有“@author 作者名”。这两个标签应当放在给类描述的文档标签中。分别表示类的版本以及作者。

  1. /**
  2. 这是一个用于演示javadoc的类
  3. @version 1.0
  4. @author ufof
  5. */
  6. public class JavaDocDemo{ //省略后面代码
复制代码
不过要注意:这是两个比较特殊的标签。所以在生成有这种标签的类时,命令语法应当如此:

  1. javadoc 源文件名 -version -author
复制代码
注:-version和-author的位置可以调换

现在我们再来看一下效果:



这四种标签是最常见的。还有一些我们暂时不会讲。

本章小结
  • 给客户提供程序,需要给予对应的程序说明书。javadoc是sun公司提供的帮助文档生成技术
  • 生成javadoc的语法如下:“javadoc 源文件名”。要被生成的类必须为public
  • 如果要给类/类的成员进行详细的说明,可以使用文档注释。文档注释语法为“/**  描述 */”
  • 如果要专门为类的作者、类的版本、方法/构造方法的参数、方法/构造方法的返回值进行专门的描述,应当使用标签


8.10 基本数据类型包装类



8.10.1 基本数据类型包装类概述

根据Java面向对象的编程思想,万事万物都是对象。但是有几个例外:那就是我们在第二章中学习过的基本数据类型,例如int、float、double、boolean这种数据类型,并非是对象。因此,Java为我们提供基本数据类型包装类。每一个基本数据类型都有与其对应的包装类。

那么我就用基本数据类型呗,不用包装类也不是不行啊?有什么用呢?包装类不仅仅可以封装基本数据类型,也可以对其对应的数据类型进行一些操作,非常方便。

基本数据类型和其包装类的对应关系:

基本数据类型
其对应的包装类
byteByte
shortShort
intInteger
longLong
charCharacter
floatFloat
doubleDouble
charCharacter

8.10.2  包装类的构造方法

基本数据类型包装类都共有这两种构造方法:

  1. 包装类(其对应的基本数据类型);  //例如:new Integer(5);
  2. 包装类(字符串);                        //例如:new Double("3.14");
复制代码
例如:

  1. class WrapClassDemo{
  2.     public static void main(String[] args){
  3.         Integer i = new Integer(4);
  4.         Boolean b = new Boolean("true");
  5.         Float f = new Float("3.2");
  6.     }
  7. }
复制代码


如果字符串中的内容不符合包装类的要求,例如new Integer("a"),会报出异常。

8.10.3 包装类的MAX_VALUE和MIN_VALUE字段

每一个包装类都会有两个被声明为public static final的字段,分别是“MAX_VALUE”和“MIN_VALUE”。这两个字段的值分别是其对应的基本数据类型的最大值和最小值。例如,int类型的最大值是2^31-1(2147483647),最小值是-2^31(-
2147483648)。那么Integer类的MAX_VALUE和MIN_VALUE中储存的值就是2147483647和-2147483648。

  1. class WrapClassDemo{
  2.     public static void main(String[] args){
  3.         System.out.println("long的最大值为:"+Long.MAX_VALUE);
  4.         System.out.println("long的最小值为:"+Long.MIN_VALUE);
  5.         
  6.         System.out.println("int的最大值为:"+Integer.MAX_VALUE);
  7.         System.out.println("int的最小值为:"+Integer.MIN_VALUE);
  8.         
  9.         System.out.println("short的最大值为:"+Short.MAX_VALUE);
  10.         System.out.println("short的最小值为:"+Short.MIN_VALUE);
  11.         
  12.         System.out.println("byte的最大值为:"+Byte.MAX_VALUE);
  13.         System.out.println("byte的最小值为:"+Byte.MIN_VALUE);
  14.     }
  15. }
复制代码
结果:



8.10.4 包装类的parseXXX()方法

parseXXX(String s)是包装类中十分常用的静态方法(XXX是包装类对应的基本数据类型)。其用于将字符串转换为基本数据类型。例如:

  1. class WrapClassDemo{
  2.     public static void main(String[] args){
  3.         String str = "432";
  4.         String str2 = "3.2";
  5.         
  6.         int x = Integer.parseInt(str);  //将str转换为int类型
  7.         System.out.println(x);
  8.         
  9.         double y = Double.parseDouble(str2);  //将str2转换为double类型
  10.         System.out.println(y);
  11.     }
  12. }
复制代码
结果:



在这个程序当中,我定义了两个字符串。通过parseInt()和parseDouble()方法将这两个字符串转换成int和double,再将其打印输出。

如果字符串中的内容是包装类对应的基本数据类型不接受的,会报出NumberFormatException异常。

8.10.5 xxxValue()方法

xxxValue()是包装类中的实例方法。其返回实例对应的基本数据类型的值。例如:

  1. class WrapClassDemo{
  2.     public static void main(String[] args){
  3.         Integer i = new Integer(10);
  4.         int i2 = i.intValue();
  5.         
  6.         System.out.println(i2);
  7.     }
  8. }
复制代码
结果:



Integer类实例i通过intValue()方法返回了其对应的int类型值,也就是10。相信这个不难理解。

包装类还有许多的有用的方法。我希望读者可以养成查阅Java API的习惯来学习(后面几节也是),所以说本节就先讲到这里。

本章小结
  • 每一个基本数据类型都有与其对应的包装类
  • 这样做的做法可以使基本数据类型拥有对象的特征
  • 包装类有两种构造方法:包装类(与其对应的基本数据类型)、包装类(字符串)
  • MAX_VALUE和MIN_VALUE是两个public static final的字段。其的值为其对应的基本数据类型的最大值和最小值
  • parseXXX(String s)方法用于将传入的字符串转换成基本数据类型的值
  • xxxValue()方法用于返回其对应的基本数据类型值





8.11 Math类



8.11.1 Math类概述

java.lang.Math类是Java为我们提供的数学相关的工具类。为了方便使用,这个类的成员都是静态。而且构造方法也被声明为private了,毕竟都是static,实例化没有任何意义。

类中的方法涵盖以下内容:
  • 三角函数以及反三角函数
  • 乘方、开方
  • 对数(以e为底、以10为底、或自定)
  • 0~1之间伪随机数
  • 角度、弧度转换
  • 其他稍微简单的运算,但是十分精确

8.11.2 pow()和sqrt()

pow()和sqrt()两个方法用于计算乘方和开方。这两个方法在类中定义为:


  • static double pow(double a, double b)
  • static double sqrt(double a)

在pow()方法中,参数a表示底数、参数b表示幂数;sqrt()中的参数a即代表要被开方的数。现在我们写一个程序验证一下:

  1. class MathDemo{
  2.     public static void main(String[] args){
  3.         System.out.println("2的5次方为"+Math.pow(2,5));
  4.         System.out.println("10的平方根为"+Math.sqrt(10));
  5.     }
  6. }
复制代码
结果:



8.11.3 toDegrees()和toRadians()

Math类提供toDegrees()和toRadians()用于转换角度和弧度。

  • static double toDegrees(double angrad)    //传入弧度返回角度
  • static double toRadians(double angreg)    //传入角度返回弧度


  1. class MathDemo{
  2.     public static void main(String[] args){
  3.         System.out.println("30度转换为弧度为"+Math.toRadians(30));
  4.         System.out.println("1度转换为角度为"+Math.toDegrees(1));
  5.     }
  6. }
复制代码
结果:



8.11.4 log()、log10()、log1p()

Math类中的这三个方法用于计算对数。

  • static double log(double a)       //返回以e为底的a的对数
  • static double log10(double a)   //返回以10为底的a的对数
  • static double log1p(double x)   //返回以e为底x+1的对数


  1. class MathDemo{
  2.     public static void main(String[] args){
  3.         System.out.println(Math.log10(100));
  4.         System.out.println(Math.log1p(9));
  5.         System.out.println(Math.log(10));
  6.     }
  7. }
复制代码
结果:



8.11.5 PI和E字段

Math类有两个被声明为public static final的字段,分别是PI和E。很显然,这两个值对应的是圆周率和自然对数。

  1. class MathDemo{
  2.     public static void main(String[] args){
  3.         System.out.println(Math.PI);
  4.         System.out.println(Math.E);
  5.     }
  6. }
复制代码
结果:



这个类的方法和字段还有很多,我不可能全部写完。希望大家可以查阅Java的API查看。

本章小结
  • java.lang.Math类是一个关于数学运算的工具类,其所有方法和字段都为静态,而且不能实例化对象
  • pow()和sqrt()用于计算乘方和开方
  • toDegrees()和toRadians()用于换算角度和弧度
  • log()、log10()、log1p()用于计算对数
  • PI和E字段储存两个数学常数:圆周率和自然对数(E)



8.12 Random类



8.12.1 Random类概述

java.util.Random类是Java类库中的一个工具类。其中的方法大多数用于生成伪随机的值,有boolean类型、long类型、int类型等。与Math类不同的是,其中的方法并不是全部static,所以说必须进行实例化。这是因为生成伪随机数需要使用到种子,每一个实例的种子都不一样。

Random类是在java.util包下,所以说必须要导包。

8.12.2 构造方法

Random类有两个构造方法:

  • Random()                       //实例化Random类
  • Random(long seed)         //实例化Random类,并指定随机种子

电脑中的随机数都是伪随机,然而必须要一个随机种子作为初始化值,通过不同迭代来形成伪随机。这两种构造方法都可以使用。

8.12.3 nextInt()

Random类的nextInt()方法用于返回一个随机的int类型数值。然而,有两个方法重载:

  • nextInt()                      //返回int类型伪随机数,范围为int类型最小值到最大值
  • nextInt(int bound)         //返回int类型伪随机数,范围为从0(包括)直到指定的数(不包括)


  1. import java.util.Random;

  2. class RandomDemo{
  3.     public static void main(String[] args){
  4.         Random r = new Random();              //实例化
  5.         for(int x = 0;x<10;x++){                  //循环十次
  6.             System.out.println(r.nextInt());      //打印伪随机数
  7.         }
  8.     }
  9. }
复制代码
结果:



这个程序中,我是用的是无参数的nextInt()方法。其所生成出来的伪随机数分布在int最小值到int最大值之间。

  1. import java.util.Random;

  2. class RandomDemo{
  3.     public static void main(String[] args){
  4.         Random r = new Random();
  5.         for(int x = 0;x<10;x++){
  6.             System.out.println(r.nextInt(100));   //生成0~99之间的伪随机数
  7.         }
  8.     }
  9. }
复制代码
结果:



这次,我是用了有参数的nextInt()方法。传入的参数为100,生成的数就是0~99之间的伪随机数。

8.12.4 nextLong()

从这个方法的字面意义上理解,就是返回一个伪随机的long类型数据。也的确是如此。不过,Random类并没有为我们提供方法重载,所以说这个方法只可以返回从long的最小值到最大值范围的伪随机数,无法指定。

  1. import java.util.Random;

  2. class RandomDemo{
  3.     public static void main(String[] args){
  4.         Random r = new Random();
  5.         for(int x = 0;x<10;x++){
  6.             System.out.println(r.nextLong());    //生成long类型伪随机数
  7.         }
  8.     }
  9. }
复制代码
结果:



这个方法我就不多说了。

8.12.5 nextBoolean()

这个方法可以返回一个随机的布尔类型。

  1. import java.util.Random;

  2. class RandomDemo{
  3.     public static void main(String[] args){
  4.         Random r = new Random();
  5.         for(int x = 0;x<10;x++){
  6.             System.out.println(r.nextBoolean());
  7.         }
  8.     }
  9. }
复制代码
结果:



很容易理解,根据结果,nextBoolean()方法会随机的返回true或false。

Random类还提供nextDouble()和nextFloat()方法,显而易见,是返回伪随机的double和float,范围是0.0到1之间。这里不再演示。

这个类就先讲到这里,对于伪随机数的生成这个类是十分方便的。希望大家能够灵活使用。

本章小结
  • java.util.Random类是关于伪随机数的工具类
  • Random()和Random(long seed)是这个类的两个构造方法。第二种构造方法在实例化的同时会指定该实例的随机种子
  • nextInt()和nextInt(int bound)都用于返回随机的int类型数字。区别在于第二个重载的方法可以指定范围,从0(包含)到指定的数(不包含)
  • nextLong()用于返回随机的long类型
  • nextBoolean()用于返回随机的布尔类型



8.13 Scanner类



8.13.1 Scanner类概述

java.util.Scanner类是一个很实用的类。其可以检测用户在命令行中的输入,也可以读取文件的内容。由于读取文件中的内容涉及到io知识,这里先暂时不讲。这一节我们着重讲检测用户的输入。

8.13.2  构造方法

Scanner类有许多构造方法,如果要用Scanner类读取用户的输入,应当使用这个构造方法:

Scanner(InputStream source)

InputStream是标准输入流,也就是System.in。在实例化时传入System.in即可。

8.13.3 next()和nextLine()方法

next()和nextLine()方法在Scanner类中如此定义:

  • String next()
  • String nextLine()

可见,这两个方法返回两个字符串。next()方法用于返回下一段字符串。“一段”是指从所在位置开始到空格结束。

  1. import java.util.Scanner;

  2. class ScannerDemo{
  3.     public static void main(String[] args){
  4.         Scanner s = new Scanner(System.in);
  5.         
  6.         System.out.println("请输入任意字符串");
  7.         System.out.println("您输入了:"+s.next());
  8.     }
  9. }
复制代码
结果:



此时光标正在闪烁,正在等待用户输入。



可见,在这个程序当中只有abc被返回,而没有def。所以说next()方法会截取从开始处到下一个空格中所有的所有字符并返回。
我们稍微修改一下代码。

  1. import java.util.Scanner;

  2. class ScannerDemo{
  3.     public static void main(String[] args){
  4.         Scanner s = new Scanner(System.in);
  5.         
  6.         System.out.println("请输入任意字符串");
  7.         System.out.println("您输入了:"+s.next());
  8.         System.out.println("您又输入了:"+s.next());
  9.     }
  10. }
复制代码
结果:




可见,在每一次调用next()方法时,其截取的开始位置会自动向前切换。
但是,如果我想要返回一整行,就要用到nextLine()方法。

  1. import java.util.Scanner;

  2. class ScannerDemo{
  3.     public static void main(String[] args){
  4.         Scanner s = new Scanner(System.in);
  5.         
  6.         System.out.println("请输入任意字符串");
  7.         System.out.println("您输入了:"+s.nextLine());
  8.     }
  9. }
复制代码
结果:





显而易见,nextLine()方法与next()的不同之处就是nextLine()会读取正行,但是next()仅仅读取一段(也就是直到空格结束)。

8.11.4 nextXXX()方法

nextXXX()中的XXX指的是一些基本数据类型,例如有nextInt(),nextLong()等等。这种方法返回的值是其对应的基本数据类型。很显然,这种方法就是在读取下一个指定的基本数据类型。

  1. import java.util.Scanner;

  2. class ScannerDemo{
  3.     public static void main(String[] args){
  4.         Scanner s = new Scanner(System.in);
  5.         
  6.         System.out.println("这是一个加法计算器。请输入一个int类型");
  7.         int a = s.nextInt();
  8.         
  9.         System.out.println("请输入另一个int类型");
  10.         int b = s.nextInt();
  11.         
  12.         System.out.println("这两个数字的和为"+(a+b));
  13.     }
  14. }
复制代码
结果:







这是我写的一个很简单的加法计算器。很容易理解,那就是让用户输入两个数字,最后将其相加,再输出即可。

本章小结
  • java.util.Scanner是可以读取用户输入,或读取文件内容的工具类
  • 要想要使用其读取用户输入,需要使用构造方法Scanner(InputStream source),也就是要传入System.in
  • next()方法用于读取从开始到下一个空格之内的所有字符,若再次调用会自动向前
  • nextLine()方法读取整行输入
  • nextXXX()中的XXX是一些基本数据类型,其读取用户输入的对应的基本数据类型。



8.12 BigInteger和BigDecimal类



8.12.1 BigInteger和BigDecimal概述

不仅仅是Java语言,许多的语言都有这样的问题:浮点数会有精度丢失。这样会导致运算浮点时出现小的偏差。

  1. class BigNumberDemo{
  2.     public static void main(String[] args){
  3.         System.out.println(0.00005+0.00002);
  4.     }
  5. }
复制代码
结果:



这是一个很简单的程序,运算0.00005+0.00002给出的结果是并不精准。BigDecimal类可以精准的运算小数,而且可以支持任何精度。

BigInteger类也是类似,它支持任何位数的整数。这两个类都提供计算四则运算、比较运算、绝对值等运算。

这两个类都在java.math包下。

8.12.2 构造方法

BigInteger类共有6个构造方法,最常用的是构造方法是:BigInteger(String val)。这种构造方法传入一个字符串,将这个字符串的内容转换为一个BigInteger。

BigDecimal的构造方法相对多一点,常见的如下:

  • BigDecimal(String val)      //将字符串的内容转换为BigDecimal(推荐)
  • BigDecimal(int val)           //将int转换为BigDecimal
  • BigDecimal(long val)         //将long转换为BigDecimal
  • BigDecimal(double val)     //将double转换为BigDecimal(不推荐,前面已经提过了浮点有精度丢失问题)


  1. import java.math.*;

  2. class BigNumberDemo{
  3.     public static void main(String[] args){
  4.         BigInteger bigInt = new BigInteger("231678641278672");    //不会溢出
  5.         BigDecimal bigDec = new BigDecimal("3.1415926535897932384626433832795028");  //不会溢出
  6.     }
  7. }
复制代码

8.12.3  四则运算

对于BigInteger和BigDecimal这两个类,都有四个共性方法,分别用于计算四则运算。

  • BigInteger add(BigInteger val)  或  BigDecimal add(BigDecimal val)                 //返回this+val
  • BigInteger subtract(BigInteger val)  或  BigDecimal subtract(BigDecimal val)     //返回this-val
  • BigInteger multiply(BigInteger val)  或  BigDecimal multiply(BigDecimal val)     //返回this*val
  • BigInteger divide(BigInteger val)  或  BigDecimal divide(BigDecimal val)         //返回this/val

然而,值得一提的是divide()方法显得有些特殊。对于BigInteger的divide(),如果结果出现小数,会向下取整。对于BigDecimal的divide()有多个方法重载,常见的两个个是:
  • BigDecimal divide(BigDecimal val, int roundingMode)
  • BigDecimal divide(BigDecimal val, RoundingMode roundingMode)          //推荐

第二个参数int roundingMode或RoundingMode roundingMode是除法运算的舍入方法。如果使用第一个方法除法运算计算出无限小数,会报出ArithmeticException异常。对于舍入方法,我在下一节中会讲述。


我们写一个实例来讲一下这四个方法。

  1. import java.math.*;

  2. class BigNumberDemo{
  3.     public static void main(String[] args){
  4.         BigInteger bigInt = new BigInteger("362178361287");
  5.         BigInteger bigInt2 = new BigInteger("65376832478");
  6.         //运算BigInteger
  7.         System.out.println("bigInt和bigInt2这两个实例的四则运算的结果分别为:");
  8.         System.out.println(bigInt.add(bigInt2).toString());         //加
  9.         System.out.println(bigInt.subtract(bigInt2).toString());   //减
  10.         System.out.println(bigInt.multiply(bigInt2).toString());   //乘
  11.         System.out.println(bigInt.divide(bigInt2).toString());      //除
  12.         
  13.         //运算BigDecimal
  14.         System.out.println("\nbigDec和bigDec2这两个实例的四则运算(除法运算除外)的结果分别为:");
  15.         BigDecimal bigDec = new BigDecimal("4327567.2623");            
  16.         BigDecimal bigDec2 = new BigDecimal("23142.46324563");        
  17.         System.out.println(bigDec.add(bigDec2).toString());                   //加
  18.         System.out.println(bigDec.subtract(bigDec2).toString());            //减
  19.         System.out.println(bigDec.multiply(bigDec2).toString());             //乘
  20.     }
  21. }
复制代码
结果:



注:如果想要将一个BigInteger或者BigDecimal类的实例的值转换成字符串,可以调用重写了的toString()实例方法。

8.14.4 BigDecimal类的舍入方式

BigDecimal类的舍入方式有两个表达形式。第一种是通过BigDecimal类的字段来表示,第二种是通过一个枚举,enum RoundingMode表示。由于第一种形式是手动实现枚举,并不推荐。这里我们使用枚举形式。

java.math.RoundingMode枚举类共有以下枚举常量:
  • CEILING             //向无穷大取整,也就是向最靠近的大于等于该数的整数
  • FLOOR               //向无穷小取整,也就是向最靠近的小于等于该数的整数
  • UP                     //若是正数,使用CEILING;若是负数,使用FLOOR
  • DOWN               //向零取整
  • HALF_DOWN      //若小数部分大于0.5使用UP;若小于0.5使用DOWN;若恰好是0.5使用DOWN
  • HALF_UP           //若小数部分大于0.5使用UP;若小于0.5使用DOWN;若恰好是0.5使用UP
  • HALF_EVEN       //若小数部分大于0.5使用UP;若小于0.5使用DOWN;若恰好是0.5使用最近的偶数
  • UNNECESSARY   //若为小数,什么都不做


(感谢@cesium_floride的提醒)

现在我们先不指定舍入方式:

  1. import java.math.*;

  2. class BigNumberDemo{
  3.     public static void main(String[] args){
  4.         BigDecimal bigDec = new BigDecimal("10");
  5.         BigDecimal bigDec2 = new BigDecimal("3");
  6.         
  7.         //10除以3位无限循环小数
  8.         System.out.println(bigDec.divide(bigDec2).toString());
  9.         
  10.     }
  11. }
复制代码
结果:



由于10/3是一个无限小数,再加上BigDecimal类没有精度限制,对于这次的运算会抛出异常。现在我们加上舍入方式,使用HALF_UP。

  1. import java.math.*;

  2. class BigNumberDemo{
  3.     public static void main(String[] args){
  4.         BigDecimal bigDec = new BigDecimal("10");
  5.         BigDecimal bigDec2 = new BigDecimal("3");
  6.         
  7.         //10除以3位无限循环小数
  8.         System.out.println(bigDec.divide(bigDec2,RoundingMode.HALF_UP).toString());   //使用HALF_UP舍入方式
  9.         
  10.     }
  11. }
复制代码
结果:



同时divide()方法还有两个方法重载:

  • BigDecimal divide(BigDecimal val, int scale, RoundingMode roundingMode)
  • BigDecimal divide(BigDecimal val, int scale, int roundingMode)

可以发现,多出来了一个参数int scale。这个参数用于指定舍入的最后小数位。

  1. import java.math.*;

  2. class BigNumberDemo{
  3.     public static void main(String[] args){
  4.         BigDecimal bigDec = new BigDecimal("10");
  5.         BigDecimal bigDec2 = new BigDecimal("3");
  6.         
  7.         //10除以3位无限循环小数
  8.         System.out.println(bigDec.divide(bigDec2,1,RoundingMode.HALF_UP).toString());   //scale=1
  9.         System.out.println(bigDec.divide(bigDec2,2,RoundingMode.HALF_UP).toString());   //scale=2
  10.         System.out.println(bigDec.divide(bigDec2,3,RoundingMode.HALF_UP).toString());   //scale=3
  11.     }
  12. }
复制代码
结果:




对于这两个类我就先讲到这里。其他的一些方法我不在赘述,希望读者可以查询API。

本章小结
  • BigInteger和BigDecimal类可以存储无精度限制的整数和分数,同时提供一些运算方法
  • add()、subtract()、multiply()、divide()分别为加减乘除
  • 对于BigInteger,使用divide()结果如果出现小数向下取整
  • 对于BigDecimal,使用divide()如果出现无限小数,而且不指定舍入方法,会报错
  • 舍入方法在RoundingMode枚举中体现



8.15 初始化块



8.15.1 初始化块概述

初始化块是类中的第五种成员。初始化块的语法为:

  1. 修饰符{
  2.     //可执行语句
  3. }
复制代码

然而,修饰符可以没有,也可以是static。如果一个初始化块没有被static修饰,就叫做初始化块;如果被static修饰了,叫做静态初始化块。

一个类可以有多个初始化块。初始化块在对象被实例化的时候被调用

  1. class InitializeCode{
  2.    
  3.     {
  4.         System.out.println("InitializeCode class got instantiated.");
  5.     }
  6. }

  7. class InitializeCodeDemo{
  8.     public static void main(String[] args){
  9.         new InitializeCode();
  10.         new InitializeCode();
  11.         new InitializeCode();
  12.     }
  13. }
复制代码
结果:





如果一个类有多个初始化块,从上到下执行。

  1. class InitializeCode{
  2.    
  3.     {
  4.         System.out.println("first");
  5.     }
  6.    
  7.     {
  8.         System.out.println("second");
  9.     }
  10.    
  11.     {
  12.         System.out.println("third");
  13.     }
  14. }

  15. class InitializeCodeDemo{
  16.     public static void main(String[] args){
  17.         new InitializeCode();
  18.     }
  19. }
复制代码
结果:





学生提问:构造方法也是在对象被调用的时候执行呀,那么初始化块不就是没有作用了吗?

回答:在实例化一个对象的时候,用户可以选择使用哪个构造方法实例化,也就是通过参数来指定。但是,调用哪个初始化块用户是选择不了的。无论使用哪个构造方法,调用的初始化块都是一样的。因此,可以把多个构造方法中的相同、重复的代码提取到初始化块中。记住:在编程当中重复的代码千万不要多写!

在子类的初始化块被调用之前,会先调用父类的初始化块,一直追朔到java.lang.Object类。

  1. class SuperClass{
  2.    
  3.     {
  4.         System.out.println("Super runs");
  5.     }
  6. }

  7. class Sub extends SuperClass{
  8.     {
  9.         System.out.println("sub runs");
  10.     }
  11. }

  12. class Sub2 extends Sub{
  13.     {
  14.         System.out.println("sub2 runs");
  15.     }
  16. }

  17. class InitializeCodeDemo{
  18.     public static void main(String[] args){
  19.         new Sub2();
  20.     }
  21. }
复制代码
结果:



8.15.2 静态初始化块

我们刚才说的初始化块是不带static修饰的。如果被static修饰就叫做静态初始化块。
当类被加载时,静态初始化块被调用。仅会调用一次。

一个类可以有多个静态初始化块,从上往下执行。

  1. class InitializeCode{
  2.    
  3.     static{
  4.         System.out.println("static code runs");
  5.     }
  6. }

  7. class InitializeCodeDemo{
  8.     public static void main(String[] args){
  9.         new InitializeCode();
  10.     }
  11. }
复制代码
结果:





在这个例子中,丝毫看不出静态初始化块和初始化块的区别。现在我们稍稍修改一下。

  1. class InitializeCode{
  2.    
  3.     static{
  4.         System.out.println("static code runs");
  5.     }
  6. }

  7. class InitializeCodeDemo{
  8.     public static void main(String[] args){
  9.         new InitializeCode();
  10.         new InitializeCode();
  11.     }
  12. }
复制代码
结果:




可以发现,我实例化了IntializeCode类两次,但是静态代码块仅仅执行了一次。这是因为,虽说实例化了两次,但是类在第一次被实例化的时候已经被加载了,不会因为每一次实例化而再加载一遍。也因此,静态代码块仅仅能被执行一次。

当子类的静态代码块被调用之前,会先调用父类的静态代码块,直到java.lang.Object类。

静态代码块中的语句也遵循着静态方法的规则:只能调用静态成员。

最后,我们再过一下类中的五种成员:
  • 字段
  • 方法
  • 构造方法
  • 内部类
  • 初始化块


本章小结
  • 初始化块是不被static修饰代码块,是类中的第五种成员
  • 初始化块在其所在的类被实例化的时候被调用
  • 构造方法中重复的语句可以放在初始化块中
  • 被static修饰代码块叫做静态代码块
  • 静态代码块在类的加载时被调用


[groupid=546]Command Block Logic[/groupid]