8.1 成员内部类
8.1.1 定义成员内部类
我们的生活中是由对象构成的。然而一类对象也可以分解成不同的几类对象。例如人类中包含脑类、心脏类、胃类等。在Java当中,内部类机制就是代表“某类的一部分”所产生的。
内部类可以轻而易举的访问外部类的成员。
内部类也算是外部类一个成员之一。
- class InnerClassDemo {
- public static void main(String[] args) {
- }
- }
- class Outer{ //外部类
- class Inner{ //内部类
-
- }
- }
从语法方面,直接在外部类中嵌套一个类即可。
8.1.2 内部类的实例化
内部类的实例化语法如下:
- 外部类.内部类 实例名 = 外部类对象.内部类对象;
在我们这个例子当中,应当是这样实例化:
- Outer.Inner i = new Outer().new Inner();
我们写一个例子:
- class InnerClassDemo {
- public static void main(String[] args) {
- Outer.Inner i = new Outer().new Inner(); //实例化内部类
- i.print(); //调用内部类print方法
- }
- }
- class Outer{
-
- class Inner{
- void print(){
- System.out.println("hello");
- }
- }
- }
在这个例子当中,我在Inner类中定义了print()方法。如果要想访问这个方法,需要实例化。所以我在主方法中,通过实例化语法实例化了Inner类,然后通过实例调用print()方法,即可。
8.1.3 内部类的访问规则
内部类可以很方便的访问外部类中的成员,即使它被声明为private。
- class InnerClassDemo2 {
- public static void main(String[] args) {
- Outer.Inner i = new Outer().new Inner();
- i.printNum();
- }
- }
- class Outer{
- private int num = 10; //定义私有变量num
- class Inner{ //内部类
- void printNum(){
- System.out.println(num);
- }
- }
- }
在这个程序当中,我在Outer类当中定义了私有int类型字段。在内部类中直接打印输出,没有任何问题。
如果内部类方法中的变量和内部类的字段冲突,优先使用局部变量。
- class InnerClassDemo2 {
- public static void main(String[] args) {
- Outer.Inner i = new Outer().new Inner();
- i.printNum();
- }
- }
- class Outer{
- class Inner{ //内部类
- int num = 1; //定义内部类字段num
- void printNum(){
- int num = 2; //定义方法中的局部变量num
- System.out.println(num);
- }
- }
- }
那么,要是我想要调用内部类的字段而非局部变量呢?用this!
- class InnerClassDemo2 {
- public static void main(String[] args) {
- Outer.Inner i = new Outer().new Inner();
- i.printNum();
- }
- }
- class Outer{
- class Inner{ //内部类
- int num = 1; //定义内部类字段num
- void printNum(){
- int num = 2; //定义方法中的局部变量num
- System.out.println(this.num);
- }
- }
- }
可以看得出来,这次打印出来了内部类的字段,而不是局部变量。
现在最大的问题是:外部类字段和内部类字段冲突了怎么办?
如果只通过this,调用的肯定是内部类的字段。但是如果我想要调用外部类的字段,需要注明是谁的this。语法是:
- 外部类.this.字段;
- class InnerClassDemo2 {
- public static void main(String[] args) {
- Outer.Inner i = new Outer().new Inner();
- i.printNum();
- }
- }
- class Outer{
- int num = 1;
- class Inner{ //内部类
- int num = 2; //定义内部类字段num
- void printNum(){
- System.out.println(Outer.this.num);
- }
- }
- }
现在,我们应该知道了如何调用外部类的成员了。
本章小结:
- 内部类机制用于表示某个类的一部分
- 内部类的实例化语法为“外部类.内部类 实例名 = 外部类对象.内部类对象”
- 如果要调用外部类的字段,需要注明是谁的this
8.2 局部内部类
8.2.1 局部内部类的定义
我们之前定义的成员内部类,这种内部类是与其他的成员同级的。如果一个内部类,它只需要在一个方法中使用,可以在方法中定义一个内部类。这种内部类叫做局部内部类。
- class Outer{ //外部类
- void outerMethod(){ //方法
- class Inner{ //局部内部类
-
- }
- }
- }
这个类当中也可以定义字段和方法。
- class Outer{ //外部类
- void outerMethod(){ //方法
- class Inner{ //局部内部类
- void innerMethod(){
- System.out.println("innerMethod() runs");
- }
- }
- }
- }
我在Inner类中定义innerMethod()。我该如何在outerMethod()中调用呢?
应该在声明局部内部类之后新建这个内部类的对象,通过实例调用。
由于只需要调用一次,可以使用匿名对象的方式。
- class Outer{ //外部类
- void outerMethod(){ //方法
- class Inner{ //局部内部类
- void innerMethod(){
- System.out.println("innerMethod() runs");
- }
- }
- new Inner().innerMethod(); //匿名对象调用innerMethod()
- }
- }
我们在主方法中应该做的事情是新建Outer对象,调用outerMethod()即可。
- class LocalInnerClassDemo {
- public static void main(String[] args) {
- new Outer().outerMethod();
- }
- }
- class Outer{ //外部类
- void outerMethod(){ //方法
- class Inner{ //局部内部类
- void innerMethod(){
- System.out.println("innerMethod() runs");
- }
- }
- new Inner().innerMethod(); //匿名对象调用innerMethod()
- }
- }
局部内部类的访问规则和成员内部类差不多,这里就不在赘述了。但是有一点:
在JDK7或以下,局部内部类要是想要访问其所在的方法的数据,其必须是final。这是因为其所在的方法结束之后,其中的数据也随之消失,但是内部类仍然在堆内存中。被声明为final的数据有特殊的储存方法,所以说其不会消失。
但是在JDK8,这个问题被修复了。字段不需要被显式的声明为final,但是其不能被重新赋值。
本章小结
- 如果一个内部类只需要被一个方法调用,可以在这个方法里面定义内部类
- 如果要调用内部类的方法/字段,需要在其所在的方法中实例化对象,才可以使用
8.3 匿名内部类
8.3.1 概念引入
请观察下列代码:
- class AnonymousInnerClassDemo{
-
- public static void main(String[] args) {
-
- }
- public static void method(MyInterface myInterface) {
- System.out.println("method() runs");
- }
- }
- interface MyInterface{
- void function();
- }
在这个程序当中,method()方法接收一个MyInterface对象的实例。我们都知道,接口不能被实例化,但是根据接口多态,我们可以传入一个MyInterface接口的实现类的实例。因此,我们需要写一个实现MyInterface的类,将这个类实例化,然后再把这个实例传入method()中。
- class AnonymousInnerClassDemo{
-
- public static void main(String[] args) {
- ImplementingClass ic = new ImplementingClass(); //实例化这个实现类
- method(ic); //传入这个实例
- }
- public static void method(MyInterface myInterface) {
- System.out.println("method() runs");
- }
- }
- class ImplementingClass implements MyInterface { //MyInterface实现类
- public void function() { //复写function()方法
- System.out.println("function() runs");
- }
- }
- interface MyInterface{
- void function();
- }
很容易发现,就为了调用一个方法,就要创建一个类。很麻烦。匿名内部类就可以在此使用。
8.3.2 匿名内部类
匿名内部类是一个“没有名字”的类的对象。通过这个对象我们可以简化上述代码。若满足下列两项,可以使用匿名内部类进行化简:
- 该类必须实现一个接口或继承一个类
- 该类只需要使用一次
匿名内部类定义方法如下:
- new 实现的接口|父类(构造方法参数){
- //在这里面复写接口或父类的抽象方法
- }
整个这一片代码都是一个对象。
我们可以通过这个形式,将method()中传入的代码简化成:
- method(new MyInterface(){
- public void function() {
- System.out.println("function() runs");
- }
- });
我这个没有名字的类,实现了MyInterface接口。因此我要new的是MyInterface。new MyInterface()之后的大括号中,就要复写MyInterface的抽象方法,也就是function()。因此,我们不需要专门写一个类去实现。可以发现这种形式简化了代码。
本章小结
- 如果需要使用一个实现某个接口的,或继承某个类的类的实例,而且只使用一次,可以使用匿名内部类简化
- 匿名内部类是一个没有名字的类的对象
- 匿名内部类的语法是:“new 接口|父类(){}”,大括号中复写接口/父类的抽象方法
8.4 Object类
8.4.1 Object类概述
Object类是十分特殊的一个类。它处于继承树的最上端。任何类都以Object类作为父类,相当于“上帝”。所以说:
- class AnyClass{}
- class AnyClass extends Object{}
因此,所有类都有Object类中定义的方法。
8.4.2 equals()方法
equals()方法接收Object类对象(因此,根据多态,他什么对象都能接收),返回布尔类型。如果调用equals()方法的实例和参数中的对象在内存中的地址一样,返回true。
我们在讲堆内存的时候,曾提到了实例指向对象。每一个对象都有自己的唯一的地址,就像是我们的身份证号。
- class ObjectDemo {
- public static void main(String[] args) {
- MyClass mc1 = new MyClass();
- MyClass mc2 = new MyClass();
- System.out.println(mc1.equals(mc2));
- }
- }
- class MyClass{}
这是为什么呢?我们在new一个对象时,其会被分配一个地址,然而地址是唯一的,两个对象不可能共享一个地址。
- class ObjectDemo {
- public static void main(String[] args) {
- MyClass mc1 = new MyClass();
- MyClass mc2 = mc1;
- System.out.println(mc1.equals(mc2));
- }
- }
- class MyClass{}
请大家看第四行。mc2这个实例,我并没有给他new一个对象赋给他,而是将之前的mc赋给了他。因此,mc和mc2都指向同一个地址。所以返回true。
这个方法经常被子类复写。例如String类中的equals()方法就复写了这个方法。
8.4.3 hashCode()
hashCode()方法不接收任何参数,返回int类型。其返回的是调用这个方法的实例的地址值(十进制)。其实equals()方法就是在比较两个实例的hashCode()是否相等。
- class ObjectDemo {
- public static void main(String[] args) {
- MyClass mc1 = new MyClass();
- MyClass mc2 = mc1;
- System.out.println(mc1.hashCode());
- System.out.println(mc2.hashCode());
- }
- }
- class MyClass{}
可以看出来,刚才equals()返回真的两个实例的hashCode值是相等的。
8.4.4 toString()方法
toString()方法不接收参数,返回字符串类型。这个方法通过字符串的表现形式,以说明这个实例。
- class ObjectDemo {
- public static void main(String[] args) {
- MyClass mc = new MyClass();
- System.out.println(mc.toString());
- }
- }
- class MyClass{}
那么,这个字符串究竟什么意思呢?
首先,我们看'@'前面,MyClass是实例的类。'@'后面是实例的十六进制地址值(我们之前的hashCode()返回的是10进制)。
关于这个方法,还有一个有趣的知识点:若直接打印一个类的实例,实际上打印的是这个实例的toString()方法。如果你想要自定义打印一个实例的结果,你可以把这个实例所在的类的toString()方法复写。
- class ObjectDemo {
- public static void main(String[] args) {
- MyClass mc = new MyClass();
- System.out.println(mc); //打印MyClass类的实例
- }
- }
- class MyClass{
- public String toString(){ //复写Object类的toString()方法
- return "哈哈哈";
- }
- }
本章小结
- Object类是所有类的父类,如果一个类不显式的extends其他类,他默认继承Object类
- Object类的equals()方法用于判断两个实例所指向的对象的地址是否相同
- hashCode()返回实例的对象的地址值(十进制)
- toString()返回:类名+@+十六进制地址
8.5 单例设计模式
8.5.1 设计模式概述
设计模式是指在实际开发当中经常使用到的、重复的、人人皆知的代码经验。目前来讲,一共有23种设计模式。单例设计模式是其中最容易理解的一种。
8.5.2 单例设计模式概述
单例设计模式应用于类当中。被应用的类叫做单例类。单例类只能有一个对象。单例类在实际开发中十分常见,许多的类只允许有一个对象。所以说这个设计模式是必须得要掌握的。
8.5.3 饿汉式
常见的单例设计模式有两种方法:饿汉式和懒汉式。 饿汗式是比较容易理解的。
首先我们需要把单例类的唯一的构造方法声明为private,这样这个类不能被new出来。
- class SingletonClass{
- private SingletonClass(){} //private构造方法
- }
- class SingletonClass{
- private static final SingletonClass instance = new SingletonClass();
- private SingletonClass(){}
- }
那么,现在我们需要定义一个方法,用于返回instance。要注意,这个方法必须为static,毕竟我们没有实例。
- class SingletonClass{
- private static final SingletonClass instance = new SingletonClass();
- private SingletonClass(){}
-
- public static SingletonClass getInstance(){
- return instance;
- }
- }
- class SingletonClass{
- private static final SingletonClass instance = new SingletonClass();
- private SingletonClass(){}
-
- public static SingletonClass getInstance(){
- return instance;
- }
- }
- class SingletonDemo{
- public static void main(String[] args){
- SingletonClass sc1 = SingletonClass.getInstance();
- SingletonClass sc2 = SingletonClass.getInstance();
-
- System.out.println(sc1.equals(sc2));
- }
- }
在主方法中的两个实例中,我都调用了SingletonClass的getInstance()方法。最后输出sc1.equals(sc2),也就是比较两者的地址值是否相等。结果为true,这证明堆内存中的确只有一个SingletonClass的对象。
那么,这个是什么原理呢?很简单,既然实例被声明为final,其永不改变。getInstance()方法只将其返回,当然是一个对象。
8.5.4 懒汉式
懒汉式是另一种做法。首先,构造方法要private,这点不多说。但是区别在于字段的实例:
- class SingletonClass{
- private static SingletonClass instance = null;
- private SingletonClass(){}
-
- }
在getInstance()方法中也有一些改变:
- class SingletonClass{
- private static SingletonClass instance = null;
- private SingletonClass(){}
-
- public static SingletonClass getInstance(){
- if(instance == null){
- instance = new SingletonClass();
- return instance;
- }
- else{
- return instance;
- }
- }
- }
添加主方法验证:
- class SingletonClass{
- private static SingletonClass instance = null;
- private SingletonClass(){}
-
- public static SingletonClass getInstance(){
- if(instance == null){
- instance = new SingletonClass();
- return instance;
- }
- else{
- return instance;
- }
- }
- }
- class SingletonDemo{
- public static void main(String[] args){
- SingletonClass sc = SingletonClass.getInstance();
- SingletonClass sc2 = SingletonClass.getInstance();
-
- System.out.println(sc.equals(sc2));
- }
- }
本章小结
- 设计模式是常用的代码经验
- 单例设计模式是让某各类(成为单例类)只能有一个对象
- 单例设计模式有两个写法:饿汉式、懒汉式
8.6 包
8.6.1 包概述
一个项目是由成千上万个类组成的,这一点不难理解。但是随之就会带来一些烦恼:类不能重名,所以说给类命名就变得麻烦。为了解决这个问题,Java为我们提供了包机制。简单的来说,包为整个项目提供层次。某些类是干某些事情的,就放在这个包中;某些类是干别的事情的,就放在另一个包中。这样不仅仅解决了命名问题,还给程序增加了层次感。可见,对包的理解是十分重要的。
8.6.2 用package声明包
package用于声明包。其必须出现在代码的第一行。紧随其后的是包名。
- package mypackage; //声明mypackage包
- class PackageDemo{
- public static void main(String[] args){
- System.out.println("hello, world");
- }
- }
我明明有PackageDemo.class文件啊?为什么提示找不到?
这是因为,包是以文件夹的形式体现出来的。由于程序第一行明确标示了“package mypackage”,其会在命令提示符所在的路径下找mypackage目录,如果找不到文件夹,自然会报错。
那么,我们需要在目前的目录下新建一个文件夹,叫做mypackage,把.class文件放进来。
现在,我们再编译一下。(注意:命令行的路径不需要改变!)
依旧是报错..怎么回事?
我们之前提过了,包用于将类分成几大部分。如果不同的包有同名的类,系统怎么知道你指的是哪个类?所以说,在Java中,类的全名是“包名.类名”。在运行时必须要声明这个是mypackage下的类,就可以通过。
在类名前面多加一个“mypackage.”就解决了这个问题。程序顺利运行。
8.6.3 让系统自动帮你创建文件夹
在上面的演示当中,我们是手动创建文件夹作为包的。但是,有一种更好的办法,可以让程序在编译时就帮你创建好文件夹。这只需要在编译(javac)时附加一点参数即可。
- javac -d 路径 源文件.java
-d是javac的附加参数之一。它用于指定放置生成的类文件的位置。后面需要跟上一个路径,这个路径便是创建文件夹的位置。
如果路径是本路径,可以用"."代替。
通过这样的方式编译源文件,便可以在指定的路径下自动创建文件夹。
8.6.4 import关键字
通过包对类进行区分后,会产生一个问题:一个类想要访问另一个包中的另一个类怎么办?
我曾经提到过,类名的全称是“包名.类名”。所以说如果要访问别的包的类,需要声明其的包名。
假如说,我在mypackage包下有一个ImportDemo类,在mypackage2包下有一个Dog类,如果需要让ImportDemo类访问Dog类,需要这样写:
- package mypackage; //声明mypackage包
- class PackageDemo{
- public static void main(String[] args){
- mypackage2.Dog myDog = new mypackage2.Dog(); //在访问Dog类时需要说明其是mypackage2包的
- myDog.bark();
- }
- }
可见,对其他的包的类访问需要在类前面加上他的包名。这样做十分麻烦。为了解决这个问题,Java为我们提供了import关键字。import关键字一般写在package之后。格式为“import 包名.类名”。当一个类被import之后,在这个类中访问被import的类不再需要声明包名。
注意:由于Dog类在其它的包中,它必须被声明为public才能被本包访问!
- <font size="3">package mypackage; //声明mypackage包
- import mypackage2.Dog; //导入mypackage2包下的Dog类
- class PackageDemo{
- public static void main(String[] args){
- Dog myDog = new Dog();
- myDog.bark();
- }
- }</font>
如果我的类需要用到另一个包中的大部分类,并不需要逐个import。可以使用星号“*”来代表该包下的所有类。
- <font size="3">package mypackage; //声明mypackage包
- import mypackage2.*; //导入mypackage2包下的所有类
- class PackageDemo{
- public static void main(String[] args){
- mypackage2.Dog myDog = new mypackage2.Dog();
- myDog.bark();
- }
- }</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语法如下:
- import static 包名.类名.静态成员;
这里我们举一个例子。我们之前一直在写的System.out.println(),其实是这样的:System是类,out是PrintStream(标准输出流类)的静态实例,println()便是PrintStream类的方法。
我们静态导入out:
- import static java.lang.System.*; //静态导入System类中的所有静态成员
- class ImportStaticDemo{
- public static void main(String[] args){
- out.println("hello"); //由于out是System的静态字段,可以不加上类名
- }
- }
本章小结
- 包用于解决类重名问题
- 包以文件夹的形式存在
- 通过“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定义语法如下:
- enum 枚举类名{
- }
定义这种枚举类的值的语法如下:
- enum 枚举类名{
- 值1,值2,值3,....值n;
- }
- enum WeeksInDay{
- MONDAY,TUESDAY,THURSDAY,WEDNESDAY,FRIDAY,SATURDAY,SUNDAY;
- }
那么,我们该如何使用这个枚举类呢?
枚举类类型定义语法:
- 枚举类 实例名 = 枚举类.枚举值;
例子:
- enum WeeksInDay{
- MONDAY,TUESDAY,THURSDAY,WEDNESDAY,FRIDAY,SATURDAY,SUNDAY;
- }
- class EnumDemo{
- public static void main(String[] args){
- WeeksInDay day = WeeksInDay.FRIDAY; //WeeksInDay类型
- }
- }
- enum WeeksInDay{
- MONDAY,TUESDAY,THURSDAY,WEDNESDAY,FRIDAY,SATURDAY,SUNDAY;
- }
- class EnumDemo{
- public static void main(String[] args){
- WeeksInDay day = WeeksInDay.FRIDAY;
- System.out.println(day); //打印day
- }
- }
8.7.3 枚举类中的一些方法
枚举类也是类,但是它没有继承Object类,而是继承了Enum类。Enum类中有两个常用的方法:
- String toString()
- int ordinal()
在我们打印输出一个枚举的值时,其实打印的是其调用toString()的返回值。所以说toString()以字符串形式返回其在枚举类中的值。
- enum WeeksInDay{
- MONDAY,TUESDAY,THURSDAY,WEDNESDAY,FRIDAY,SATURDAY,SUNDAY;
- }
- class EnumDemo{
- public static void main(String[] args){
- WeeksInDay day = WeeksInDay.FRIDAY;
- String str = day.toString(); //将day实例的toString()方法的返回值赋给String类的实例str
-
- System.out.println(str); //打印str
- }
- }
ordinal()方法返回的是实例对应的值的序号。枚举中的每一个值都有对应的序号,从0开始排列,与数组的下标一个道理。
- enum WeeksInDay{
- MONDAY,TUESDAY,THURSDAY,WEDNESDAY,FRIDAY,SATURDAY,SUNDAY;
- }
- class EnumDemo{
- public static void main(String[] args){
- WeeksInDay day = WeeksInDay.FRIDAY;
-
- System.out.println(day);
- System.out.println(day.ordinal()); //打印day对应的序号
- }
- }
由于FRIDAY是WeeksInDay的第五个值,其的序号是4(毕竟从0开始数)。
8.7.4 接收枚举的switch
在第三章,我们学习过多分支语句switch。switch除了接收一些基本数据类型外,还接收枚举类。其通过枚举类的值执行对应的语句。语法我就不说了,见第三章。
- enum WeeksInDay{
- MONDAY,TUESDAY,THURSDAY,WEDNESDAY,FRIDAY,SATURDAY,SUNDAY;
- }
- class EnumDemo{
- public static void main(String[] args){
- WeeksInDay day = WeeksInDay.TUESDAY;
-
- switch(day){
- case MONDAY:
- System.out.println("今天星期一");
- break;
- case TUESDAY:
- System.out.println("今天星期二");
- break;
- case THURSDAY:
- System.out.println("今天星期三");
- break;
- case WEDNESDAY:
- System.out.println("今天星期四");
- break;
- case FRIDAY:
- System.out.println("今天星期五");
- break;
- case SATURDAY:
- System.out.println("今天星期六");
- break;
- case SUNDAY:
- System.out.println("今天星期天");
- break;
- }
- }
- }
本章小结
- 枚举类用于表现只有特定的值的数据类型
- 枚举类对应的关键字是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类如此定义:
- package mypackage2;
- public class Test{
- int x; //default权限字段
- public int y; //public权限字段
- }
现在,我们在mypackage包下的PermissionDemo类中实例化Test类,访问这两个字段:
- package mypackage;
- import mypackage2.Test;
- class PermissionDemo{
- public static void main(String[] args){
- Test t = new Test();
- t.y = 10;
- t.x = 5; //非法
- }
- }
由于PermissionDemo类在Test类在不同的包中,PermissionDemo无权访问default的字段x。因此发生编译时错误。如果我们将x声明为public,这个问题就能得到解决。
8.8.3 protected
protected从权限上来讲,仅仅比default多一点点。protected也是同包内可以访问,区别在于,不同包的子类可以继承protected成员。
假设我们把刚才的Test类改成这样:
- package mypackage2;
- public class Test{
- protected static int x; //protected权限字段
- }
PermissionDemo改成:
- package mypackage;
- import mypackage2.Test;
- class PermissionDemo extends Test{ //继承Test类
- public static void main(String[] args){
- x = 5; //对继承过来的字段赋值
- System.out.println(x);
- }
- }
'
可以看得出来,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
为了方便演示,我先定义这样的一个类:
- public class JavaDocDemo{
- //字段
- public int x;
- private String str;
- int y;
- //方法
- public void method(){}
- protected static int method2(){ return 0;}
- private String method3(){return "hello";}
- //构造方法
- public JavaDocDemo(){}
- public JavaDocDemo(int x, int y){}
- }
生成javadoc的基本命令如下:
- javadoc 源文件名
如果想要在指定的目录下生成javadoc,命令如下:
- javadoc -d 目录 源文件名
如果类不在任何包下,不需要输入包名。
注意:要被生成javadoc的类必须声明为public!
好的,现在我们通过第一个命令,生成一下刚才我们定义的类的帮助文档。
在键入命令之后,命令行会显示出创建时的信息。如果没有报出错误,javadoc帮助文档应当正确的被生成。
index.html是所有其他的html文件的集合。我们用自己的浏览器打开看一下:
方法特写:
字段特写:
构造方法特写:
不难发现:只有某些成员被显示出来了,有一些没有。这是为什么?
在javadoc中,只有被声明为public或protected的成员才会被显示。
点开任意方法/构造方法的超链接,可以查看详细信息:
不过可以发现,任何一个方法都没有一个详细的说明。这是因为我们没有写。那么具体是在哪里写呢?这里涉及到我们在第一章中提到的:文档注释。
8.9.3 文档注释以及标签
文档注释一般放在类和类的成员之前。用于给这个成员加上一个详细的描述。格式如下:
- /**
- 描述
- */
和多行注释的区别是:多行注释开头只有一个星号*,文档注释有两个。
我们现在给类和类中的成员都添加一个文本注释:
- /**
- 这是一个用于演示javadoc的类
- */
- public class JavaDocDemo{
- /**
- 这是一个int类型字段x
- */
- public int x;
-
- /**
- 这是一个String类型字段str
- */
- private String str;
- int y;
-
- /**
- 这是一个公共、无返回值的方法method()
- */
- public void method(){}
-
- /**
- 这是一个protected、静态、返回int的方法method2()
- */
- protected static int method2(){ return 0;}
- private String method3(){return "hello";}
-
- /**
- 这是一个无参的构造方法
- */
- public JavaDocDemo(){}
-
- /**
- 这是一个接收两个int类型的构造方法
- */
- public JavaDocDemo(int x, int y){}
- }
好的,现在我们给类中的成员都加上了其对应的文档注释(这次会有一些警告,是因为没有加上标签,不过现在不需要理会)。重新生成javadoc,再来看看:
方法摘要以及字段摘要:
那么,现在我们已经给类以及类的成员都添加了一个描述。但是一个方法/构造方法是有可能有参数以及返回值的,我们也需要对这些参数和返回值进行特别的描述。为了增强排版,可以使用标签来对这几个参数/返回值进行描述。
参数的标签为“@param 参数名 描述”,返回值的标签为“@return 描述”。这几个标签需要放在文档注释中。
- /**
- 这是一个protected、静态、返回int的方法method2()
- @param a 这是一个int类型参数
- @param str 这是一个String类型参数
- @return 这个方法会返回0
- */
- protected static int method2(int a, String str){ return 0;}
-
- /**
- 这是一个接收两个int类型的构造方法
- @param x 这是一个int类型参数
- @param y 这是另一个int类型参数
- */
- public JavaDocDemo(int x, int y){}
以及:
可以发现,参数以及返回值可以通过文档注释中的标签来给予一个描述。
常用的标签还有两个:“@version 版本号”,还有“@author 作者名”。这两个标签应当放在给类描述的文档标签中。分别表示类的版本以及作者。
- /**
- 这是一个用于演示javadoc的类
- @version 1.0
- @author ufof
- */
- public class JavaDocDemo{ //省略后面代码
- javadoc 源文件名 -version -author
现在我们再来看一下效果:
这四种标签是最常见的。还有一些我们暂时不会讲。
本章小结
- 给客户提供程序,需要给予对应的程序说明书。javadoc是sun公司提供的帮助文档生成技术
- 生成javadoc的语法如下:“javadoc 源文件名”。要被生成的类必须为public
- 如果要给类/类的成员进行详细的说明,可以使用文档注释。文档注释语法为“/** 描述 */”
- 如果要专门为类的作者、类的版本、方法/构造方法的参数、方法/构造方法的返回值进行专门的描述,应当使用标签
8.10 基本数据类型包装类
8.10.1 基本数据类型包装类概述
根据Java面向对象的编程思想,万事万物都是对象。但是有几个例外:那就是我们在第二章中学习过的基本数据类型,例如int、float、double、boolean这种数据类型,并非是对象。因此,Java为我们提供基本数据类型包装类。每一个基本数据类型都有与其对应的包装类。
那么我就用基本数据类型呗,不用包装类也不是不行啊?有什么用呢?包装类不仅仅可以封装基本数据类型,也可以对其对应的数据类型进行一些操作,非常方便。
基本数据类型和其包装类的对应关系:
基本数据类型 | 其对应的包装类 |
byte | Byte |
short | Short |
int | Integer |
long | Long |
char | Character |
float | Float |
double | Double |
char | Character |
8.10.2 包装类的构造方法
基本数据类型包装类都共有这两种构造方法:
- 包装类(其对应的基本数据类型); //例如:new Integer(5);
- 包装类(字符串); //例如:new Double("3.14");
- class WrapClassDemo{
- public static void main(String[] args){
- Integer i = new Integer(4);
- Boolean b = new Boolean("true");
- Float f = new Float("3.2");
- }
- }
如果字符串中的内容不符合包装类的要求,例如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。
- class WrapClassDemo{
- public static void main(String[] args){
- System.out.println("long的最大值为:"+Long.MAX_VALUE);
- System.out.println("long的最小值为:"+Long.MIN_VALUE);
-
- System.out.println("int的最大值为:"+Integer.MAX_VALUE);
- System.out.println("int的最小值为:"+Integer.MIN_VALUE);
-
- System.out.println("short的最大值为:"+Short.MAX_VALUE);
- System.out.println("short的最小值为:"+Short.MIN_VALUE);
-
- System.out.println("byte的最大值为:"+Byte.MAX_VALUE);
- System.out.println("byte的最小值为:"+Byte.MIN_VALUE);
- }
- }
8.10.4 包装类的parseXXX()方法
parseXXX(String s)是包装类中十分常用的静态方法(XXX是包装类对应的基本数据类型)。其用于将字符串转换为基本数据类型。例如:
- class WrapClassDemo{
- public static void main(String[] args){
- String str = "432";
- String str2 = "3.2";
-
- int x = Integer.parseInt(str); //将str转换为int类型
- System.out.println(x);
-
- double y = Double.parseDouble(str2); //将str2转换为double类型
- System.out.println(y);
- }
- }
在这个程序当中,我定义了两个字符串。通过parseInt()和parseDouble()方法将这两个字符串转换成int和double,再将其打印输出。
如果字符串中的内容是包装类对应的基本数据类型不接受的,会报出NumberFormatException异常。
8.10.5 xxxValue()方法
xxxValue()是包装类中的实例方法。其返回实例对应的基本数据类型的值。例如:
- class WrapClassDemo{
- public static void main(String[] args){
- Integer i = new Integer(10);
- int i2 = i.intValue();
-
- System.out.println(i2);
- }
- }
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即代表要被开方的数。现在我们写一个程序验证一下:
- class MathDemo{
- public static void main(String[] args){
- System.out.println("2的5次方为"+Math.pow(2,5));
- System.out.println("10的平方根为"+Math.sqrt(10));
- }
- }
8.11.3 toDegrees()和toRadians()
Math类提供toDegrees()和toRadians()用于转换角度和弧度。
- static double toDegrees(double angrad) //传入弧度返回角度
- static double toRadians(double angreg) //传入角度返回弧度
- class MathDemo{
- public static void main(String[] args){
- System.out.println("30度转换为弧度为"+Math.toRadians(30));
- System.out.println("1度转换为角度为"+Math.toDegrees(1));
- }
- }
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的对数
- class MathDemo{
- public static void main(String[] args){
- System.out.println(Math.log10(100));
- System.out.println(Math.log1p(9));
- System.out.println(Math.log(10));
- }
- }
8.11.5 PI和E字段
Math类有两个被声明为public static final的字段,分别是PI和E。很显然,这两个值对应的是圆周率和自然对数。
- class MathDemo{
- public static void main(String[] args){
- System.out.println(Math.PI);
- System.out.println(Math.E);
- }
- }
这个类的方法和字段还有很多,我不可能全部写完。希望大家可以查阅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(包括)直到指定的数(不包括)
- import java.util.Random;
- class RandomDemo{
- public static void main(String[] args){
- Random r = new Random(); //实例化
- for(int x = 0;x<10;x++){ //循环十次
- System.out.println(r.nextInt()); //打印伪随机数
- }
- }
- }
这个程序中,我是用的是无参数的nextInt()方法。其所生成出来的伪随机数分布在int最小值到int最大值之间。
- import java.util.Random;
- class RandomDemo{
- public static void main(String[] args){
- Random r = new Random();
- for(int x = 0;x<10;x++){
- System.out.println(r.nextInt(100)); //生成0~99之间的伪随机数
- }
- }
- }
这次,我是用了有参数的nextInt()方法。传入的参数为100,生成的数就是0~99之间的伪随机数。
8.12.4 nextLong()
从这个方法的字面意义上理解,就是返回一个伪随机的long类型数据。也的确是如此。不过,Random类并没有为我们提供方法重载,所以说这个方法只可以返回从long的最小值到最大值范围的伪随机数,无法指定。
- import java.util.Random;
- class RandomDemo{
- public static void main(String[] args){
- Random r = new Random();
- for(int x = 0;x<10;x++){
- System.out.println(r.nextLong()); //生成long类型伪随机数
- }
- }
- }
这个方法我就不多说了。
8.12.5 nextBoolean()
这个方法可以返回一个随机的布尔类型。
- import java.util.Random;
- class RandomDemo{
- public static void main(String[] args){
- Random r = new Random();
- for(int x = 0;x<10;x++){
- System.out.println(r.nextBoolean());
- }
- }
- }
很容易理解,根据结果,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()方法用于返回下一段字符串。“一段”是指从所在位置开始到空格结束。
- import java.util.Scanner;
- class ScannerDemo{
- public static void main(String[] args){
- Scanner s = new Scanner(System.in);
-
- System.out.println("请输入任意字符串");
- System.out.println("您输入了:"+s.next());
- }
- }
此时光标正在闪烁,正在等待用户输入。
可见,在这个程序当中只有abc被返回,而没有def。所以说next()方法会截取从开始处到下一个空格中所有的所有字符并返回。
我们稍微修改一下代码。
- import java.util.Scanner;
- class ScannerDemo{
- public static void main(String[] args){
- Scanner s = new Scanner(System.in);
-
- System.out.println("请输入任意字符串");
- System.out.println("您输入了:"+s.next());
- System.out.println("您又输入了:"+s.next());
- }
- }
可见,在每一次调用next()方法时,其截取的开始位置会自动向前切换。
但是,如果我想要返回一整行,就要用到nextLine()方法。
- import java.util.Scanner;
- class ScannerDemo{
- public static void main(String[] args){
- Scanner s = new Scanner(System.in);
-
- System.out.println("请输入任意字符串");
- System.out.println("您输入了:"+s.nextLine());
- }
- }
显而易见,nextLine()方法与next()的不同之处就是nextLine()会读取正行,但是next()仅仅读取一段(也就是直到空格结束)。
8.11.4 nextXXX()方法
nextXXX()中的XXX指的是一些基本数据类型,例如有nextInt(),nextLong()等等。这种方法返回的值是其对应的基本数据类型。很显然,这种方法就是在读取下一个指定的基本数据类型。
- import java.util.Scanner;
- class ScannerDemo{
- public static void main(String[] args){
- Scanner s = new Scanner(System.in);
-
- System.out.println("这是一个加法计算器。请输入一个int类型");
- int a = s.nextInt();
-
- System.out.println("请输入另一个int类型");
- int b = s.nextInt();
-
- System.out.println("这两个数字的和为"+(a+b));
- }
- }
这是我写的一个很简单的加法计算器。很容易理解,那就是让用户输入两个数字,最后将其相加,再输出即可。
本章小结
- java.util.Scanner是可以读取用户输入,或读取文件内容的工具类
- 要想要使用其读取用户输入,需要使用构造方法Scanner(InputStream source),也就是要传入System.in
- next()方法用于读取从开始到下一个空格之内的所有字符,若再次调用会自动向前
- nextLine()方法读取整行输入
- nextXXX()中的XXX是一些基本数据类型,其读取用户输入的对应的基本数据类型。
8.12 BigInteger和BigDecimal类
8.12.1 BigInteger和BigDecimal概述
不仅仅是Java语言,许多的语言都有这样的问题:浮点数会有精度丢失。这样会导致运算浮点时出现小的偏差。
- class BigNumberDemo{
- public static void main(String[] args){
- System.out.println(0.00005+0.00002);
- }
- }
这是一个很简单的程序,运算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(不推荐,前面已经提过了浮点有精度丢失问题)
- import java.math.*;
- class BigNumberDemo{
- public static void main(String[] args){
- BigInteger bigInt = new BigInteger("231678641278672"); //不会溢出
- BigDecimal bigDec = new BigDecimal("3.1415926535897932384626433832795028"); //不会溢出
- }
- }
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异常。对于舍入方法,我在下一节中会讲述。
我们写一个实例来讲一下这四个方法。
- import java.math.*;
- class BigNumberDemo{
- public static void main(String[] args){
- BigInteger bigInt = new BigInteger("362178361287");
- BigInteger bigInt2 = new BigInteger("65376832478");
- //运算BigInteger
- System.out.println("bigInt和bigInt2这两个实例的四则运算的结果分别为:");
- System.out.println(bigInt.add(bigInt2).toString()); //加
- System.out.println(bigInt.subtract(bigInt2).toString()); //减
- System.out.println(bigInt.multiply(bigInt2).toString()); //乘
- System.out.println(bigInt.divide(bigInt2).toString()); //除
-
- //运算BigDecimal
- System.out.println("\nbigDec和bigDec2这两个实例的四则运算(除法运算除外)的结果分别为:");
- BigDecimal bigDec = new BigDecimal("4327567.2623");
- BigDecimal bigDec2 = new BigDecimal("23142.46324563");
- System.out.println(bigDec.add(bigDec2).toString()); //加
- System.out.println(bigDec.subtract(bigDec2).toString()); //减
- System.out.println(bigDec.multiply(bigDec2).toString()); //乘
- }
- }
注:如果想要将一个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的提醒)
现在我们先不指定舍入方式:
- import java.math.*;
- class BigNumberDemo{
- public static void main(String[] args){
- BigDecimal bigDec = new BigDecimal("10");
- BigDecimal bigDec2 = new BigDecimal("3");
-
- //10除以3位无限循环小数
- System.out.println(bigDec.divide(bigDec2).toString());
-
- }
- }
由于10/3是一个无限小数,再加上BigDecimal类没有精度限制,对于这次的运算会抛出异常。现在我们加上舍入方式,使用HALF_UP。
- import java.math.*;
- class BigNumberDemo{
- public static void main(String[] args){
- BigDecimal bigDec = new BigDecimal("10");
- BigDecimal bigDec2 = new BigDecimal("3");
-
- //10除以3位无限循环小数
- System.out.println(bigDec.divide(bigDec2,RoundingMode.HALF_UP).toString()); //使用HALF_UP舍入方式
-
- }
- }
同时divide()方法还有两个方法重载:
- BigDecimal divide(BigDecimal val, int scale, RoundingMode roundingMode)
- BigDecimal divide(BigDecimal val, int scale, int roundingMode)
可以发现,多出来了一个参数int scale。这个参数用于指定舍入的最后小数位。
- import java.math.*;
- class BigNumberDemo{
- public static void main(String[] args){
- BigDecimal bigDec = new BigDecimal("10");
- BigDecimal bigDec2 = new BigDecimal("3");
-
- //10除以3位无限循环小数
- System.out.println(bigDec.divide(bigDec2,1,RoundingMode.HALF_UP).toString()); //scale=1
- System.out.println(bigDec.divide(bigDec2,2,RoundingMode.HALF_UP).toString()); //scale=2
- System.out.println(bigDec.divide(bigDec2,3,RoundingMode.HALF_UP).toString()); //scale=3
- }
- }
对于这两个类我就先讲到这里。其他的一些方法我不在赘述,希望读者可以查询API。
本章小结
- BigInteger和BigDecimal类可以存储无精度限制的整数和分数,同时提供一些运算方法
- add()、subtract()、multiply()、divide()分别为加减乘除
- 对于BigInteger,使用divide()结果如果出现小数向下取整
- 对于BigDecimal,使用divide()如果出现无限小数,而且不指定舍入方法,会报错
- 舍入方法在RoundingMode枚举中体现
8.15 初始化块
8.15.1 初始化块概述
初始化块是类中的第五种成员。初始化块的语法为:
- 修饰符{
- //可执行语句
- }
然而,修饰符可以没有,也可以是static。如果一个初始化块没有被static修饰,就叫做初始化块;如果被static修饰了,叫做静态初始化块。
一个类可以有多个初始化块。初始化块在对象被实例化的时候被调用。
- class InitializeCode{
-
- {
- System.out.println("InitializeCode class got instantiated.");
- }
- }
- class InitializeCodeDemo{
- public static void main(String[] args){
- new InitializeCode();
- new InitializeCode();
- new InitializeCode();
- }
- }
如果一个类有多个初始化块,从上到下执行。
- class InitializeCode{
-
- {
- System.out.println("first");
- }
-
- {
- System.out.println("second");
- }
-
- {
- System.out.println("third");
- }
- }
- class InitializeCodeDemo{
- public static void main(String[] args){
- new InitializeCode();
- }
- }
学生提问:构造方法也是在对象被调用的时候执行呀,那么初始化块不就是没有作用了吗?
回答:在实例化一个对象的时候,用户可以选择使用哪个构造方法实例化,也就是通过参数来指定。但是,调用哪个初始化块用户是选择不了的。无论使用哪个构造方法,调用的初始化块都是一样的。因此,可以把多个构造方法中的相同、重复的代码提取到初始化块中。记住:在编程当中重复的代码千万不要多写!
在子类的初始化块被调用之前,会先调用父类的初始化块,一直追朔到java.lang.Object类。
- class SuperClass{
-
- {
- System.out.println("Super runs");
- }
- }
- class Sub extends SuperClass{
- {
- System.out.println("sub runs");
- }
- }
- class Sub2 extends Sub{
- {
- System.out.println("sub2 runs");
- }
- }
- class InitializeCodeDemo{
- public static void main(String[] args){
- new Sub2();
- }
- }
8.15.2 静态初始化块
我们刚才说的初始化块是不带static修饰的。如果被static修饰就叫做静态初始化块。
当类被加载时,静态初始化块被调用。仅会调用一次。
一个类可以有多个静态初始化块,从上往下执行。
- class InitializeCode{
-
- static{
- System.out.println("static code runs");
- }
- }
- class InitializeCodeDemo{
- public static void main(String[] args){
- new InitializeCode();
- }
- }
在这个例子中,丝毫看不出静态初始化块和初始化块的区别。现在我们稍稍修改一下。
- class InitializeCode{
-
- static{
- System.out.println("static code runs");
- }
- }
- class InitializeCodeDemo{
- public static void main(String[] args){
- new InitializeCode();
- new InitializeCode();
- }
- }
可以发现,我实例化了IntializeCode类两次,但是静态代码块仅仅执行了一次。这是因为,虽说实例化了两次,但是类在第一次被实例化的时候已经被加载了,不会因为每一次实例化而再加载一遍。也因此,静态代码块仅仅能被执行一次。
当子类的静态代码块被调用之前,会先调用父类的静态代码块,直到java.lang.Object类。
静态代码块中的语句也遵循着静态方法的规则:只能调用静态成员。
最后,我们再过一下类中的五种成员:
- 字段
- 方法
- 构造方法
- 内部类
- 初始化块
本章小结
- 初始化块是不被static修饰代码块,是类中的第五种成员
- 初始化块在其所在的类被实例化的时候被调用
- 构造方法中重复的语句可以放在初始化块中
- 被static修饰代码块叫做静态代码块
- 静态代码块在类的加载时被调用
[groupid=546]Command Block Logic[/groupid]