Giter Club home page Giter Club logo

java-note's Introduction

java-note's People

Contributors

zuiliushang avatar

Watchers

James Cloos avatar  avatar

java-note's Issues

一起来了解数组吧

  • 数组是一种引用数据类型,数组引用变量只是一个引用,数组元素和数组变量在内存里是分开存放的。这是介绍数组在内存中的运行机制的笔记。

内存中的数组

数组引用变量只是一个引用,这个引用变量可以指向任何有效的内存,只有当该引用指向有效内存后,才可通过该数组变量来访问数组元素。

与所有引用变量相同的是,引用变量是访问真实对象的根本方式。也就是说,如果希望在程序中访问数组对象本身,则只能通过这个数组的引用变量来访问它。

实际的数组对象被存储在堆(heap)内存中;如果引用该数组对象的数组引用变量是一个局部变量,那么它被存储在栈(stack)内存中。
11

  • 栈内存和堆内存:

当一个方法被调用时,方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁。因此,所有在方法中定义的局部变量都是放在栈内存中的;在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存结束后,这个对象还可能被另一个引用变量所引用(在方法的参数传递时很常见),则这个对象依然不会被销毁。只有当一个对象没有任何引用变量引用它时,系统的垃圾回收器才会在合适的时候回收它。

如果堆内存中数组不再有任何引用变量指向自己,则这个数据将成为垃圾,该数组所占的内存将会被系统的垃圾回收机制回收。因此,为了让垃圾回收机制回收一个数组所占的内存空间,可以将该数组变量赋为 null ,也就切断了数组引用变量和实际数组之间的引用关系,实际的数组也就去成了垃圾。

只要类型相互兼容,就可以让一个数组变量指向另一个实际的数组,这种操作会让人产生数组的长度可变的错觉。下面是一个Demo:

public class Demo1 {


    public static void main(String arg[]){

        int[] a = {1,2,3,4};
        int[] b = new int[5];

        System.out.println("数组B的长度:"+b.length);//输出结果为5

        b=a;
        System.out.println("数组B的长度:"+b.length);//输出结果为4
    }
}
  • 原因是这样的:

当程序定义并初始化了 a、b 两个数组后,系统内存中实际上产生了4个内存区(栈内存和堆内存各两个),其中栈内存中有两个引用变量:a 和 b ; 堆内存中也有两块内存区,分别用于存储a 和b 引用所指向的数组本身:

12

当执行b = a 语句时,系统将会把 a 的值赋给 b ,a 和 b 都是引用类型变量,存储的是地址。阴齿把 a 的值赋给 b 后,就是让 b 指向 a 所指向的地址。

13

此时:b 原来指向的数组因失去了所有的引用,变成了垃圾,只有等待垃圾回收机制来回收它。

  • 接下来我们来看看不同类型的数组的初始化吧。

基本类型数组的初始化。

对于基本来兴数组,数组元素的值是直接存储在对应的数组元素中,因此,在初始化数组时,先为该数组分配内存空间,然后直接将数组元素的值存入对应数组元素中。

Demo:

public class Demo2 {
    public static void main(String args[]){
        int[] arr;
        arr = new int[5];
        for(int i=0;i<arr.length;i++){
            arr[i] = i+1;
        }


    }
}

当执行第一行代码int[] arr; 时 仅仅只是在栈内存中定义了一个空引用,这个引用并没有任何有效的内存,当然无法执行数组的长度:
14

当执行了第二行代码arr = new int[5]; 动态初始化后,系统将负责为该数组分配内存空间,并分配默认的初始值,所有数组元素都被赋值为 0 :
15

此时 arr 数组的每个元素的值都是 0 ,当循环为该数组的每个元素依次赋值后,此时每个数组元素的值都变成程序显式指定的值:
16

引用类型数组的初始化。

引用类型数组的数组元素是引用,因此情况变得更加复杂。每个数组元素里面存储的还是引用,它指向另一块内存,这块内存里存储了有效数据。

我们来看下这个Demo:

public class Demo3{
    public static void main(String args[]){
        Person[] person;
        person = new Person[2];
        Person zhangsan = new Person();
        zhangsan.age=1;
        zhangsan.height=11;

        Person lisi = new Person();
        lisi.age=2;
        lisi.height=22;

        person[0] = zhangsan;
        person[1] = lisi;
        //输出一样的
        lisi.info();
        person[0].info();
    }
}

class Person {
    public int age;//年龄
    public double height;//身高

    //定义一个info 方法
    public void info(){
        System.out.println("我的身高:"+ age + "我的年龄:" + height);
    }
}

当执行Person[] person;代码时,仅仅在栈内存中定义了一个引用变量,也就是一个指针,这个指针并未指向任何有效的内存区:

17

而当执行person = new Person[2]; 时,程序对 person 数组执行动态初始化,动态初始化由系统为数组元素分配默认的初始值:null,即每个数组元素的值都是 null:

18

可以看出 person 数组的两个元素都是引用,因此每个数组元素的值都是 null ,这意味着依然不能直接使用 person 数组的元素(因为都是引用,还没有指定对应的堆内存中具体的内存区)。接着执行语句

Person zhangsan = new Person();
        zhangsan.age=1;
        zhangsan.height=11;

        Person lisi = new Person();
        lisi.age=2;
        lisi.height=22;

定义了zhangsan 和 lisi 两个 Person 实例,定义这两个实例实际上分配了 4 块内存,在栈内存中存储了 zhangsan 和 lisi 两个引用变量,在堆内存中存储了两个 Person 实例。

19

此时 数组 person 的两个数组元素依然是 null , 直到程序运行 person[0] = zhangsan; person[1] = lisi; 时,程序依次将 zhangsan 赋给 person 数组的第一个元素,把lisi 赋给 person 数组的第二个元素, person 数组的两个元素将会指向有效的内存区:

20

多维数组

多维数组其实也是一维数组,只不过只不过数组里面的引用元素指向的内存区的数据是一维数组。

看下面的一个Demo:

21

了解泛型

了解泛型

所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型仓鼠,也可称为类型实参)。Java 5 改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。

定义泛型接口、类

包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑上的子类,但这种子类在物理上并不存在。

  public class Apple<T> {
    // 使用 T 形参定义实例变量
    private T info;
    public Apple(){}

    // 使用T类型形参来定义构造器
    public Apple(T info){
        this.info = info;
    }

    /**
     * @return the info
     */
    public T getInfo() {
        return info;
    }
    /**
     * @param info the info to set
     */
    public void setInfo(T info) {
        this.info = info;
    };

    public static void main(String[] args) {
        Apple<String> apple = new Apple<String>("苹果");
        System.out.println(apple.getInfo());
        Apple<Double> apple2 = new Apple<Double>(55.5);
        System.out.println(apple2.getInfo());
    }
}

当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。例如,为 Apple类定义构造器,器构造器名称依然是 Apple , 而不是Apple! 调用该构造器时却可以使用 Apple的形式,当然应该为 T 形参传入实际的类型参数。

从泛型类派生子类

  • 当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类,需要指出的是,当使用这些接口、父类时不能再包含类型形参。例如,下面的代码是错误的:
    //定义类A继承 Apple 类,Apple 类不能跟类型形参
    public class A extends Apple<T>{ }

方法中的形参代表变量、常量、表达式等数据。定义方法时可以声明数据形参,调用方法时必须为这些数据形参传入实际的数据;与此类似的是,定义类、接口、方法时可以声明类型形参,使用类、接口、方法时应该为类型形参传入实际的类型。

  • 如果想从 Apple 类派生一个子类,则可以改为如下代码:
    //使用 Apple 类时,为 T 形参传入 String 具体的类型
    public class A extends Apple<String>
  • 调用方法时必须为所有的数据形参传入参数值,与调用方法不同的是,使用类、接口时也可以部委类型形参传入实际的类型参数,即也可以写成这样:
    // 使用 Apple 类时,没有为 T 形参传入实际的类型参数
    public class A extends Apple

实际上例如:

public class A2 extends Apple{
    //重写父类方法
    public String getInfo(){
        return super.getInfo().toString();
    }
}

Java 8 新增的 Lambda 表达式

Lambda 表达式

  • Lambda 表达式是 Java 8 的重要更新,也是一个被广大开发者期待已久的新特性。 Lambda 表达式支持将代码块作为方法参数, Lambda 表达式允许使用简洁的代码来创建只有一个抽象方法的接口(这种接口被称为函数式接口) 的实例。

Lambda 表达式入门

首先是使用匿名内部类:

public interface Command
{
    // 接口里定义的process()方法用于封装“处理行为”
    void process(int[] target);
}

public class ProcessArray
{
    public void process(int[] target , Command cmd)
    {
        cmd.process(target);
    }
}

public class Demo10 {
    public static void main(String[] args){
        ProcessArray pa = new ProcessArray();
        int[] target = {1,2,3,4,5};
        pa.process(target,new Command()
                {
                    public void process(int[] target){
                        int sum = 0 ;
                        for(int tmp:target){
                            sum = sum + tmp;
                        }
                        System.out.println(sum);
                    }
                });
    }
}

解读: ProcessArray 类的 process() 方法处理数组时,希望可以动态传入一段代码作为具体的处理行为,因此程序创建了一个 匿名内部类 实例来封装处理行为。

我们可以用 Lambda 表达式来简化创建匿名内部类对象:

public class Demo10 {
    public static void main(String[] args){
        ProcessArray pa = new ProcessArray();
        int[] target = {1,2,3,4,5};
        pa.process(target, (int[] target)->{
            int sum = 0 ;
            for(int tmp:target){
                sum = sum + tmp;
            }
            System.out.println(sum);
        });
/*      pa.process(target,new Command()
                {
                    public void process(int[] target){
                        int sum = 0 ;
                        for(int tmp:target){
                            sum = sum + tmp;
                        }
                        System.out.println(sum);
                    }
                });*/
    }
}

从程序中代码可以看出,用 Lambda 表达式改写的代码与创建匿名内部类时需要实现的 process(int[] target) 方法完全相同,只是不需要 new 方法名(){} 这么繁琐的代码,不需要指出重写方法的名字,也不需要给出重写的方法的返回值类型 —— 只要给出重写的方法括号以及括号里的形参列表即可。

  • 通过例子可以看出: 当使用 Lambda 表达式代替匿名内部类创建对象时,Lambda 表达式的代码块将会代替实现抽象方法的方法体,Lambda 表达式就相当于一个匿名方法。

事实上,Lambda 表达式的主要作用就是代替匿名内部类的繁琐语法,它由三部分组成:

  • 形参列表。形参列表允许省略形参类型。如果形参列表中只有一个形参,甚至连形参列表的圆括号都可以省略。
  • 箭头(->) 。 必须通过英文中画线和大于符号组成。
  • 代码块。 如果代码块只包含一条语句,Lambda 表达式允许省略代码块的花括号。
    // Lambda 表达式的代码块只有一条语句,则可以省略花括号。
    Person.eat(()->System.out.println("好吃不上火QAQ"));
    // Lambda 表达式的形参列表只有一个形参,可以省略圆括号
    Person.driver(weather->{
        System.out.println("呀呀呀:" + weather);
        System.out.println("么么哒 Java QAQ 不要虐");
    })
    // Lambda 表达式的代码块只有一条语句,可以省略花括号。
    // 代码块中只有一条语句,即使该表达式需要返回值,也可以省略 return 关键字。
    Person.count((a , b)->a + b);

Lambda 表达式和函数式接口

Lambda 表达式的类型,也被称为 “目标类型(target type)”,Lambda 表达式的目标类型必须是“函数式接口(functional interface)”。函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。

对象与垃圾回收

对象与垃圾回收

当程序创建对象、数组等引用类型实体时,系统都会在堆内存中为之分配一块内存区,对象就保存在这块内存区中,当这块内存不再被任何引用变量引用时,这块内存就变成垃圾,等待垃圾回收机制进行回收。垃圾回收机制特征:

  • 垃圾回收机制只负责回收堆内存中的对象,不会回收任何物理资源(例如数据库连接、网络IO等资源)。
  • 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候进行。当对象永久性地失去引用后,系统就会在合适的时候回收它所占的内存。
  • 在垃圾回收机制回收任何对象之前,总会先调用它的 finalize() 方法,该方法可能使该对象重新复活(让一个引用变量重新引用该对象),从而导致垃圾回收机制取消回收。

对象在内存中的状态

当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种:

  • 可达状态:当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中出于可达状态,程序可用过引用变量来调用该对象的实例变量和方法。
  • 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象的 finalize() 方法进行资源清理。如果系统在调用 finalize() 方法时重新让一个引用变量引用该对象,则这个对象会再次变为可达状态;否则该对象进入不可达状态。
  • 不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的 finalize() 方法后依然没有使该对象编程可达状态,那么这个对象将永久性地失去引用,最后变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源

强制垃圾回收

当一个对象失去引用后,系统何时调用它的 finalize() 方法对它进行资源清理,何时它会变成不可达状态,系统何时回收它所占有的内存,对于程序完全透明。程序只能控制一个对象何时不再被任何引用变量引用,决不能控制它何时被回收。

程序无法精确控制Java 垃圾回收的时机 ,但可以强制系统进行垃圾回收——**这种强制只是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定。**大部分时候,程序强制系统垃圾回收后总会有一些效果。强制系统垃圾回收的方法:

  • 调用 System 类的 gc() 静态方法: System.gc()。
  • 调用 Runtime 对象的 gc() 实例方法:Runtime.getRuntime().gc()。

finalize 方法

  • 在垃圾回收机制回收某个对象所占用的内存之前,通常要求程序调用适当的方法来清理资源,在没有明确指定清理资源的情况下,Java 提供了默认机制来清理该对象的资源,这个机制就是 finalize() 方法,方法原型:
protected void finalize() throws Throwable

当finalize() 方法返回后,对象消失,垃圾回收机制开始执行。

任何 Java 类都可以重写 Object 类的 finalize() 方法,在该方法中清理该对象占用的资源。如果程序终止之前始终没有进行垃圾回收,则不会调用失去引用对象的 finalize() 方法来清理资源。垃圾回收机制何时调用对象的 finalize() 方法时完全透明的,只有当程序认为需要更多的额外内存时,垃圾回收机制才会进行垃圾回收。因此,完全有可能出现一种情况:某个失去引用的对象只占用了少量内存,而且系统没有产生严重的内存需求,因此垃圾回收机制并没有试图回收该对象所占用的资源,所以该对象的 finalize() 方法也不会得到调用。

finalize() 方法具有如下 4 个特点:

  • 永远不要主动调用某个对象的 finalize() 方法,该方法应交给垃圾回收机制调用。
  • finalize() 方法何时被调用,是否被调用具有不确定性,不要把 finalize() 方法当成一定会被执行的方法。
  • 当 JVM 执行可恢复对象的 finalize() 方法时,可能使该对象或系统中其他对象重新变成可达状态。
  • 当 JVM 执行 finalize() 方法时出现异常时,垃圾回收机制不会报告异常,程序继续执行。

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

对象的软、弱和虚引用。

对大部分对象而言,程序里会有一个引用变量引用该对象,这是最常见的引用方式。除此之外,java.lang.ref 包下提供了 3 个类: SoftReference、phantomReference 和 WeakReference,它们分别代表了系统对对象的 3 种引用方式:软引用、虚引用和弱引用

  • 强引用(StrongReference)

这是 Java 程序中最常见的引用方式。程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象。当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收。

  • 软引用(SoftReference)

软引用需要通过 SoftReference 类来实现,当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用对象而言,当系统内存空间足够时,它不会被系统回收,程序也可以使用该对象;当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中。

  • 弱引用(WeakReference)

弱引用通过 WeakReference 类实现,弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存,当然,并不是说当一个对象只有弱引用时,它就立即被回收——正如那些失去引用的对象一样,必须等待到系统垃圾回收机制运行时才会被回收

  • 虚引用(PhantomReference)

虚引用通过 PhantomReference 类实现,虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那么它也没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和**引用队列(ReferenceQueue)**联合使用。

三种引用类都包含了一个 get() 方法,用于获取被他们所引用的对象。

一起来了解数组吧

  • 数组是一种引用数据类型,数组引用变量只是一个引用,数组元素和数组变量在内存里是分开存放的。这是介绍数组在内存中的运行机制的笔记。

内存中的数组

数组引用变量只是一个引用,这个引用变量可以指向任何有效的内存,只有当该引用指向有效内存后,才可通过该数组变量来访问数组元素。

与所有引用变量相同的是,引用变量是访问真实对象的根本方式。也就是说,如果希望在程序中访问数组对象本身,则只能通过这个数组的引用变量来访问它。

实际的数组对象被存储在堆(heap)内存中;如果引用该数组对象的数组引用变量是一个局部变量,那么它被存储在栈(stack)内存中。

11

  • 栈内存和堆内存:

    当一个方法被调用时,方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁。因此,所有在方法中定义的局部变量都是放在栈内存中的;在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存结束后,这个对象还可能被另一个引用变量所引用(在方法的参数传递时很常见),则这个对象依然不会被销毁。只有当一个对象没有任何引用变量引用它时,系统的垃圾回收器才会在合适的时候回收它。

    如果堆内存中数组不再有任何引用变量指向自己,则这个数据将成为垃圾,该数组所占的内存将会被系统的垃圾回收机制回收。因此,为了让垃圾回收机制回收一个数组所占的内存空间,可以将该数组变量赋为 null ,也就切断了数组引用变量和实际数组之间的引用关系,实际的数组也就去成了垃圾。

    只要类型相互兼容,就可以让一个数组变量指向另一个实际的数组,这种操作会让人产生数组的长度可变的错觉。

Java程序运行机制

Java程序运行机制

  • Java 语言是一种特殊的高级语言,它既有解释型语言的特征,也具有编译型语言的特征,因为 Java 程序要经过先编译,后解释两个步骤。

高级语言的运行机制

计算机高级语言按程序的执行方式可以分为编译型和解释型两种。

编译型语言是指使用专门的编译器,针对特定平台(如操作系统)将某种高级语言源代码一次性“翻
译”成可悲该平台硬件执行的机器码(包括机器指令和操作数),并包装成该平台所能识别的可执行性程
序的格式,这个转换过程称之为编译(Compile)。编译生成的可执行程序可以脱离开发环境,在特定
的平台上独立运行。

有些程序编译结束后,还可能需要对其他编译好的目标代码进行链接,即组装两个以上的目标代码模
块生成最终的可执行程序,通过这种方式实现低层次的代码复用。

因为编译型语言是一次性地编译成机器码,所以可以脱离开发环境独立运行,而且通常运行效率较高;但因为编译型语言的程序被编译成特定平台上的机器码,因此编译生成的可执行行程序通常无法移植到其他平台上运行;如果需要移植,则必须将源代码复制到特定平台上,针对特定平台进行修
改,至少也需要采用特定平台上的编译器重新编译。

现有的C、C++、Objective-C、Pascal 等高级语言都属于编译型语言。

解释型语言是指使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行的语言。解释型
语言通常不会进行整体性的编译和链接处理,解释型语言相对于把编译型语言中的编译和解释过程混合
在一起同时完成。

可以认为:每次执行解释型语言的程序都需要进行一次编译,因此解释型语言的程序运行效率通常较
低,而且不能脱离解释器独立运行。但解释器负责将源程序解释成特定平台的机器指令即可。解释型语
言可以方便地实现源程序级的移植,但这是以牺牲程序执行效率为代价的。

现有的 Ruby、Python 等语言都属于解释型语言。

除此之外,还有一种伪编译型语言,如 Visual Basic ,它属于半编译型语言,并不是真正的编译型语
言。它首先被编译成P-代码,并将解释引擎封装在可执行性程序内,当运行程序时,P-代码会被解析成
真正的二进制代码。表面上看起来 , Visual Basic 可以编译生成可执行的 EXE 文件,而且这个 EXE 文
件也可以脱离开发环境,在特定平台上运行,非常像编译型语言。实际上,在这个 EXE 文件中,既有
程序的启动代码,也有链接解释程序的代码,而这部分代码负责启动 Vsiual Basic 解释程序,再对
Visual Basic 代码进行解释并执行。

Java 程序的运行机制和 JVM

Java 语言比较特殊,由 Java 语言编写的程序需要经过编译步骤,但这个编译步骤并不会生成特定平
台的机器码,而是生成一种与平台无关的字节码(也就是*.class 文件)。当然,这种字节码不是可执行
性的,必须使用 Java 解释器来解释执行。因此可以认为: Java 语言既是编译型语言,也是解释型语
言。或者换个角度:Java 既不是纯粹的解释器语言,也不是纯粹的编译型语言。Java 程序的执行过程
必须经过先编译、后解释两个步骤:

qq 20160305162736

Java 语言负责解释执行字节码文件的是 Java 虚拟机,即JVM(Java Virtual Machine)。JVM 是可
以运行 Java 字节码文件的虚拟计算机。所有平台上的 JVM 向编译器提供相同的程序接口,而编译器只
需要面向虚拟机,生成虚拟机能理解的代码,然后由虚拟机来解释执行。在一些虚拟机的实现中,还会
降虚拟机代码转换成特定系统的机器码执行,从而提高执行效率。

当使用 Java 编译器编译 Java 程序时,生成的是与平台无关的字节码,这些字节码不面向任何具体
平台,只面向 JVM 。不同平台上的JVM 都是不同的,但是他们都提供了相同的接口。JVM 是 Java 程
序跨平台的关键部分,只要为不同平台实现了相应的虚拟机,编译后的 Java 字节码就可以在该平台上
运行。显然,相同的字节码程序需要在不用的平台上运行,这几乎是“不可能“的,只有通过中间的转换
器才可以实现,JVM 就是这个转换器。

JVM 是一个抽象的计算机,和实际的计算机一样。,它具有指令集并使用不同的存储区域。它负责
执行指令,还要管理数据、内存和寄存器。

深入构造器

深入构造器

构造器是一个特殊的方法,这个特殊方法用于创建实例化时执行初始化,构造器是创建对象的重要途径(即使使用工厂模式、反射等方式创建对象,其实质依然是依赖于构造器),因此,Java 类必须包含一个或一个以上的构造器。

使用构造器进行初始化

构造器最大的用处就是在创建对象时执行初始化。当创建一个对象时,系统为这个对象的实例变量进行默认初始化,这种默认的初始化把所有基本类型的实例变量设为 0 (对数值型实例变量) 或 false (布尔型实例变量),把所有引用类型的实例变量设为 null。

如果想改变这种默认的初始化,想让系统创建对象时就为该实例变量显式指定初始值,就可以通过构造器来实现。

如果程序员没有为 Java 提供任何构造器,则系统会为这个类提供一个无参数的构造器,这个构造器的执行体为空。无论如何,Java 类至少包含一个构造器。

而一旦程序员提供了自定义的构造器,系统就不再提供默认的构造器,因此一般如果有提供有参的构造器,还需提供一个无参的构造器哦哦~。

因为构造器主要用于被其他方法调用,用以返回该类的实例,因而通常把构造器设置成 public 访问权限,从而允许系统中任何位置的类来创建该类的对象。除非在一些极端的情况下,业务需要限制创建该类的对象,可以把构造器设置成其他访问权限,例如设置为 protected ,主要用于被其子类调用:把其设置为 private ,组织其他类创建该类的实例。

构造器重载

同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为构造器重载。构造器重载允许 Java 类里包含多个初始化逻辑,从而允许使用不同的构造器来初始化 Java 对象。

构造器重载的方法和方法重载基本相似:只要求构造器的名字相同;为了让系统区分不同的构造器,多个构造器的参数列表必须不同。

JAVA 格式化

JAVA 格式化

使用 NumberFormat 格式化数字。

MessageFormat 是抽象类 Format 的子类,Format 抽象类还有两个子类:NumberFormat 和 DateFormat,它们分别用于实现数值、日期的格式化。NumberFormat、DateFormat 可以将数值、日期转换成字符串,也可以将字符串转换成数值、日期。

12

NumberFormat 也是一个抽象基类,所以无法通过它的构造器来创建 NumberFormat 对象,它提供了如下几个类方法来得到 NumberFormat 对象:

  • getCurrencyInstance():返回默认 Locale 的货币格式器。也可以在调用该方法时传入指定的 Locale ,则获取指定Locale 的货币格式器。
  • getIntegerInstance(): 返回默认 Locale 的整数格式器。也可以在调用该方法时传入指定的 Locale,则获取指定 Locale 的整数格式器。
  • getNumberInstance():返回默认 Locale 的通用数值格式器。也可以在调用该方法时传入指定的Locale,则获取指定 Locale 的通用数值格式器。
  • getPercentInstance():返回默认 Locale 的百分数格式器。也可以在调用该方法时传入指定的 Locale,则获取指定 Locale 的百分数格式器。

示例代码:

public class NumberFormatDemo {
    public static void main(String[] args){
        //需要被格式化的数字
        double db = 12306.123;
        //创建4个 Locale 分别代表 **、日本、德国、美国
        Locale[] locales = 
                new Locale[]{Locale.CHINA,Locale.JAPAN,Locale.GERMAN,Locale.US};
        //为4个 Locale 创建12个NumberFormat 对象
        NumberFormat[] numberFormats = new NumberFormat[12];
        //每个 Locale 分别有通用数值格式器、百分数格式器、货币格式器
        for (int i = 0; i < locales.length; i++) {
            numberFormats[i*3] = NumberFormat.getNumberInstance(locales[i]);
            numberFormats[i*3+1] = NumberFormat.getPercentInstance(locales[i]);
            numberFormats[i*3+2] = NumberFormat.getCurrencyInstance(locales[i]);
        }

        for (int i = 0; i < locales.length; i++) {
            String tip = i == 0? "--------**的格式-------":
                i==1?"--------日本的格式-------":
                    i==2?"--------德国的格式-------":"--------美国的格式-------";
            System.out.println(tip);
            System.out.println("通用数值格式:"
                    + numberFormats[i*3].format(db));
            System.out.println("百分比数值格式:"
                    + numberFormats[i*3+1].format(db));
            System.out.println("货币数值格式:"
                    + numberFormats[i*3+2].format(db));
        }
    }
}

使用 DateFormat 格式化日期、时间。

DateFormat 也是一个抽象类,它提供了如下方法:

每种方法后面可以传入多个参数,用于指定样式和 Locale等参数;如果不指定则使用默认样式。

  • getDateInstance():返回一个日期格式器,它格式化后的字符串只有日期,没有时间。
  • getTimeInstance(): 返回一个时间格式器,它格式化后的字符串只有时间,没有日期。
  • getDateTimeInstance():返回一个日期、时间格式器,它格式化后的字符串既有日期,也有时间。

实例:

public class DateFormatDemo {
    public static void main(String[] args){
        //创建一个需要被格式化的时间
        Date date = new Date();
        //创建两个 Locale 代表 **、美国
        Locale[] locales = new Locale[]{Locale.CHINA,Locale.US};
        DateFormat[] dateFormats = new DateFormat[16];
        //位两个 Locale 创建 16 个 DateFormat 对象
        for (int i = 0; i < locales.length; i++) {
            dateFormats[i*8] = DateFormat.getDateInstance(DateFormat.SHORT,locales[i]);
            dateFormats[i*8+1] = DateFormat.getDateInstance(DateFormat.MEDIUM, locales[i]);
            dateFormats[i*8+2] = DateFormat.getDateInstance(DateFormat.LONG,locales[i]);
            dateFormats[i*8+3] = DateFormat.getDateInstance(DateFormat.FULL,locales[i]);
            dateFormats[i*8+4] = DateFormat.getTimeInstance(DateFormat.SHORT, locales[i]);
            dateFormats[i*8+5] = DateFormat.getTimeInstance(DateFormat.MEDIUM, locales[i]);
            dateFormats[i*8+6] = DateFormat.getTimeInstance(DateFormat.LONG, locales[i]);
            dateFormats[i*8+7] = DateFormat.getTimeInstance(DateFormat.FULL, locales[i]);
        }
        for (int i = 0; i < locales.length; i++) {
            String tip = i==0?"---------**日期格式---------":
                "---------美国日期格式---------";
            System.out.println(tip);
            System.out.println("SHORT 格式的日期格式:"+dateFormats[i*8].format(date));
            System.out.println("MEDIUM 格式的日期格式:"+dateFormats[i*8+1].format(date));
            System.out.println("LONG 格式的日期格式:"+dateFormats[i*8+2].format(date));
            System.out.println("FULL 格式的日期格式:"+dateFormats[i*8+3].format(date));
            System.out.println("SHORT 格式的时间格式:"+dateFormats[i*8+4].format(date));
            System.out.println("MEDIUM 格式的时间格式:"+dateFormats[i*8+5].format(date));
            System.out.println("LONG 格式的时间格式:"+dateFormats[i*8+6].format(date));
            System.out.println("FULL 格式的时间格式:"+dateFormats[i*8+7].format(date));
        }
    }
}

输出结果:

---------**日期格式---------
SHORT 格式的日期格式:16-4-1
MEDIUM 格式的日期格式:2016-4-1
LONG 格式的日期格式:2016年4月1日
FULL 格式的日期格式:2016年4月1日 星期五
SHORT 格式的时间格式:下午3:05
MEDIUM 格式的时间格式:15:05:55
LONG 格式的时间格式:下午03时05分55秒
FULL 格式的时间格式:下午03时05分55秒 CST
---------美国日期格式---------
SHORT 格式的日期格式:4/1/16
MEDIUM 格式的日期格式:Apr 1, 2016
LONG 格式的日期格式:April 1, 2016
FULL 格式的日期格式:Friday, April 1, 2016
SHORT 格式的时间格式:3:05 PM
MEDIUM 格式的时间格式:3:05:55 PM
LONG 格式的时间格式:3:05:55 PM CST
FULL 格式的时间格式:3:05:55 PM CST

DateFormat 的 parse() 方法可以把一个字符串解析成 Date 对象,但它要求被解析的字符串必须符合日期字符串的要求,否则可能抛出 ParseException 异常。

使用 SimpleDateFormat 格式化日期。(常用)

  • 虽然 DateFormat 的 parse() 方法可以把字符串解析成 Date 对象,但实际上 DateFormat 的 parse() 方法不够灵活——它要求被解析的字符串必须满足特定的格式!

SimpleDateFormat 类是 DateFormat 的子类。

SimpleDateFormat 可以非常灵活地格式化 Date , 也可以用于解析各种格式的日期字符串。创建 SimpleDateFormat 对象时候需要传入一个 pattern 字符串,这个 pattern 字符串不是正则表达式,而是一个日期模板字符串:

public class SimpleDateFormatDemo {
    public static void main(String[] arg) throws ParseException{
        Date date = new Date();
        //创建一个 SimpleDateFormat 对象
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("Gyyyy年中第 D 天");
        // 将 date 格式化成 日期,输出:公园 2016 年中第 92天
        String dataString = simpleDateFormat.format(date);
        System.out.println(dataString);
        // 将字符串 解析成日期
        dataString = "16####四月!!01";
        SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("y####MMM!!d");
        System.out.println(simpleDateFormat2.parse(dataString));
    }
}

final 修饰符

final 修饰符

final 关键字可用于修饰类、变量和方法,用于表示它修饰的类、方法和变量不可改变。

final 修饰变量时,表示该变量一旦获得了初始值就不可改变。

final 成员变量

成员变量是随类初始化或对象初始化而初始化的。当类初始化时,系统会为该类的类变量分配内存,并分配默认值;当创建对象时,系统会为该对象的实例变量分配内存,并分配默认值。也就是说,当执行静态初始化块时可以对类变量赋初始值;当执行普通初始化块、构造器时刻对实例变量赋初始值。因此,成员变量的初始值可以在定义该变量时指定默认值,也可以在初始化块、构造器中指定初始值。

对于 final 修饰的成员变量而言,一旦有了初始值,就不能被重新赋值,如果既没有在定义成员变量时制定初始值,也没有在初始化块、构造器中为成员变量指定初始值,那么这些成员变量的值将一直是系统默认分配的0、'\u0000'、 false 或 null , 这些成员变量也就完全失去了存在的意义。因此 Java 语言规定: final 修饰的成员变量必须由程序员显式地指定初始值

综上所述,final 修饰的类变量、实例变量能指定初始值的地方如下:

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

final 修饰的实例变量,要么在定义该实例变量时指定初始值,要么在普通初始化块或构造器中为该实例变量指定初始值。但是:如果普通初始化块已经为某个实例变量指定了初始值,则不能再在构造器中为该实例变量指定初始值;final 修饰的类变量,要么在定义该类变量时指定初始值,要么在静态初始化块中为该类变量指定初始值。

final 局部变量

系统不会对局部变量进行初始化,局部变量必须由编程人员显式初始化。因此使用 final 修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。

如果 final 修饰的局部变量在定义时没有指定默认值,则可以在后面代码中对该 final 变量赋初始值,但只能一次,不能重复赋值;

final 修饰基本类型变量和引用类型变量的区别

当使用 final 修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用, final 只保证这个应用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。

可执行“宏替换” 的 final 变量

对一个 final 变量来说,不管它是类变量、实例变量,还是局部变量,只要该变量满足三个条件,这个 final 变量就不再是一个变量,而是相当于一个直接量。

    public static void main(String[] args){
        final int a = 5;
        System.out.println(a);
    }

实际上 变量 a 根本不存在, 当程序执行 System.out.println(a);实际上执行的是System.out.println(5);

final 修饰符的一个重要用途就是定义 “宏变量” 。当定义 final 变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个 final 变量本质就是一个 “宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。

使用JAR文件

使用JAR文件

jar 命令详解

  • 创建 JAR 文件:jar cf test.jar test
  • 创建 JAR 文件,并显示压缩过程:jar cvf test.jar test
  • 不使用清单文件: jar cvfM test.jar test
  • 自定义清单文件内容: jar cvfm test.jar manifest.mf test
  • 查看 JAR 包内容: jar tf test.jar
  • 查看 JAR 包详细内容: jar tvf test.jar
  • 解压缩: jar xf test.jar
  • 带提示信息解压缩: jar xvf test.jar
  • 更新 JAR 文件: jar uf test.jar Hello.class
  • 更新时显示详细信息: jar uvf test.jar Hello.class

系统相关-System 类和Runtime 类

系统相关-System 类和Runtime 类

  • Java 程序再不同操作系统上运行时,可能需要取得平台相关的属性,或者调用平台命令来完成特定功能。 Java 提供了 System 类和 Runtime 类来与程序的运行平台进行交互

System 类

  • System 类代表当前 Java 程序的运行平台,程序不能创建 System 类的对象,System 类提供了一些类变量和类方法,允许直接通过 System 类来调用这些类变量和类方法。
  • System 类提供了代表标准输入、标准输出和错误输出的类变量,并提供了一些静态方法用于访问环境变量、系统属性的方法,还提供了加载文件和动态链接库的方法。

通过 System 类来访问操作系的环境变量和系统属性:

public class SystemClassDemo1 {
    public static void main(String[] args){
        Map<String, String> env = System.getenv();
        for (String name : env.keySet()) {
            System.out.println(name + "=" + env.get(name));
        }
    }
}

加载文件和动态链接库主要对 native 方法有用,对于一些特殊的功能(如访问操作系统底层硬件设备等) Java 程序无法实现,必须借助 C 语言来完成,此时需要使用 C 语言为 Java 方法提供实现。:
1.Java 程序中声明 native 修饰的方法,类似于 abstract 方法,只有方法签名,没有实现。编译该 Java 程序,生成一个 class 文件。
2.用 javah 编译第1步生成的 class 文件,将产生一个 .h 文件。
3.写一个 .cpp 文件实现 native 方法,这一步需要包含第2步产生的 .h 文件(这个.h 文件中又包含了JDK 带的jni.h 文件)。
4.将第3步的.cpp 文件编译成动态链接库文件。
5.在Java 中用 System 类的 loadLibrary..()方法或者 Runtime 类的 loadLibrary() 方法加载第4步产生的动态链接库文件, Java 程序中就可以调用这个 native 方法了。

Runtime 类

  • Runtime 类代表 Java 程序的运行时环境,每个 Java 程序都有一个与之对应的 Runtime 实例,应用程序通过该对象与其运行时环境相连。应用程序不能创建自己的 Runtime 实例,但可以通过getRuntime() 方法获取与之关联的 Runtime 对象。
  • 与 System 类似的是, Runtime 类也提供了 gc() 方法和 runFinalization() 方法来通知系统进行垃圾回收、清理系统资源,并提供了 load(String filename) 和 loadLibrary(String libname) 方法来加载文件和动态链接库。

Runtime 类代表 Java 程序运行时环境,可以访问 JVM 的相关信息,如处理器数量,内存信息等:

public class RuntimeClassDemo1 {
    public static void main(String[] args){
        Runtime rt = Runtime.getRuntime();
        System.out.println("处理器数量:"+rt.availableProcessors());
        System.out.println("空闲内存数:"+rt.freeMemory());
        System.out.println("总内存数:"+rt.totalMemory());
        System.out.println("可用最大内存数:"+rt.maxMemory());
    }
}

异常类的继承体系

异常类的继承体系

Java 常见的异常类之间的继承关系:
011

例子:

public class ExceptionDemo {
    public static void main(String[] args){
        try {
            int a = Integer.parseInt(args[0]);
            int b = Integer.parseInt(args[1]);
            int c = a/b;
            System.out.println("a/b="+c);
        } catch (IndexOutOfBoundsException e) {
            e.printStackTrace();
            System.out.println("数组越界");
        } catch (NumberFormatException e) {
            e.printStackTrace();
            System.out.println("数字格式异常:程序只能接受整数参数");
        }catch (ArithmeticException e) {
            e.printStackTrace();
            System.out.println("算术异常");
        }catch (Exception e) {
            e.printStackTrace();
            System.out.println("未知异常");
        }
    }
}

上面程序中的三种异常:数组越界(IndexOutOfBoundsException),数字格式异常(NumberFormatException),算术异常(ArithmeticException)都是常见的异常,记住。

  • 异常捕获时,记得先捕获小异常,再捕获大异常
public class ExceptionDemo1 {
    public static void main(String[] args) {
        try {
            System.out.println(111);
        } catch (RuntimeException e) {
            System.out.println("运行时异常");
        } 
        //报错 编译错误
        //Unreachable catch block for NullPointerException. 
        //It is already handled by the catch block for RuntimeException
        catch (NullPointerException e) {
            System.out.println("空指针异常");
        }
    }
}

Java 7 提供的多异常捕获

在 Java 7 以前,每个 catch 块只能捕获一种类型的异常;但从 Java 7 开始,一个 catch 块可以捕获多种类型的异常。

使用一个 catch 块捕获多种类型的异常时需要注意如下两个地方。

  • 捕获多种类型异常时,多种异常类型之间用 "|" 隔开。
  • 捕获多种类型异常时,异常变量有隐式的 final 修饰,因此程序不能对异常变量重新赋值。
public class ExceptionDemo2 {
    public static void main(String[] args){
        try {
            int a = Integer.parseInt(args[0]);
            int b = Integer.parseInt(args[1]);
            int c = a/b;
            System.out.println("a/b="+c);
        } catch (IndexOutOfBoundsException|NumberFormatException|ArithmeticException ie) {
            ie.printStackTrace();
            System.out.println("数组越界或者数字格式异常或者算术异常");
            //The parameter ie of a multi-catch block cannot be assigned 报错
            //ie = new ArithmeticException("哈哈");  
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("未知异常");
            e = new RuntimeException("哦哦");//不报错
        }
    }
}

访问异常信息

如果程序需要在 catch 块中访问异常对象的相关信息,则可以通过访问 catch 块的后异常形参来获得。当 Java 运行时决定调用某个 catch 块来处理该异常对象时,会将异常对象赋给 catch 块后的异常参数,程序即可通过该参数来获得异常的相关信息。

所有的异常对象都包含了如下几个常用方法:

  • getMessage():返回该异常的详细描述字符串。
  • printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
  • printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
  • getStackTrace():返回该异常的跟踪栈信息。

Checked 异常和 Runtime 异常体系

Java 的异常被分为两大类: Checked 异常和 Runtime 异常(运行时异常)。所有的 RuntimeException 类及其子类的实例被称为 Runtime 异常;不是 RuntimeException 类及其子类的异常实例则被称为 Checked 异常。

对于 Checked 异常的处理方式有如下两种:

  • 当前方法明确知道如何处理该异常,程序应该使用 try……catch 块来捕获该异常,然后在对应的 catch 块中修复该异常。
  • 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。

只有 Java 语言提供了 Checked 异常, Checked异常体现了 Java 的严谨性,它要求程序猿必须注意该异常 —— 要么显式声明抛出,要么显式捕获并处理它,总之不允许对 Checked 异常不闻不问。这是一种非常严谨的设计,可以增加程序的健壮性。但是大部分的方法总是不能明确地知道如何处理异常,因此只能声明抛出该异常,而这种情况非常普遍,所以 Checked 异常降低了程序开发的生产率和代码的执行效率。

使用 throws 声明抛出异常

使用 throws 声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果 main 方法也不知道如何处理这种类型的异常,也可以使用 throws 声明抛出异常,该异常将交给 JVM 处理。

JVM处理异常的方法是:打印异常的跟踪栈信息,并且中断程序运行!

使用 throw 抛出异常

当程序出现错误时,系统会自动抛出异常;除此之外, Java 也允许程序自行抛出异常,自行抛出异常使用 throw 语句来完成(不是 throwsssssssssssss)。

抛出异常

很多时候,系统是否要抛出异常,可能需要根据应用的业务需求来决定,如果程序中的数据、执行与既定的业务需求不符,这就是一种异常。由于与业务需求不符而产生的异常,必须由程序猿来决定抛出,系统无法抛出这种异常。

如果需要在程序中自行抛出异常,则应使用 throw 语句, throw 语句可以单独使用, throw 语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例:

    throw ExceptionInstance;
public class ExceptionDemo3 {
    public static void main(String[] args) throws DIYException {
        int i ;
        try {
            i=1/0;
        } catch (RException e) {
            e.printStackTrace();
            throw new DIYException("错了错了");
        }
    }
}

异常处理规则

  • 使程序代码混乱最小化。
  • 捕获并保留诊断信息。
  • 通知合适的人员。
  • 采用合适的方式结束异常活动

修饰符的适用范围

修饰符的适用范围

外部类/接口属性方法构造器初始化块成员内部类局部成员
public
protected
包访问控制符
private
abstract
final
static
strictfp
synchronized
native
transient
volatile
default

Anntation 注释/注解(基本 Annotation)

Anntation 注释/注解

从 JDK 5 开始, Java 增加了对元数据 (MetaData) 的支持,也就是 Annotation(注释、注解),Annotation 其实是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行响应的处理。通过使用注解,程序可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署。

Annotation 提供了一种伪程序元素设置元数据的方法,从某些方面来看, Annotation 就像是修饰符一样,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被存储在 Annotation 的 “name=value” 对中。

Annotation 是一个接口,程序可以通过反射来获取指定程序元素的 Annotation 对象,然后通过 Annotation 对象来取得注解里的元数据。要注意的是,有的 Annotation 指的是 java.lang.Annotation 接口,有的指的是 注解本身。

基本 Annotation

Annotation 必须使用工具处理,工具负责提取 Annotation 里包含的元数据,工具还会根据这些元数据增加额外的功能。

Java提供的5个基本 Annotation 的用法 —— 使用 Annotation 时要在其前面增加 @ 符号,并把该 Annotation 当成一个修饰符来使用,用于修饰它支持的程序元素。

5个基本的 Annotation 如下:

限定重写父类方法:@OverRide

@OverRide 就是用来指定方法覆盖的,它可以强制一个子类必须覆盖父类的方法:

public class Fruit {
    public void info(){
        System.out.println("我是水果");
    }
}

class Apple extends Fruit{
    @Override
    public void info(){
        System.out.println("我是苹果");
    }
}

如果没有覆盖父类方法,那么将会报错,可以保证子类绝对会覆盖父类方法,可以防止覆盖方法写错的问题。

@OverRide 只能修饰方法,不能修饰其他程序元素。

标示已过时:@deprecated

@deprecated 用于标示某个程序元素(类、方法等)已过时,当其他程序使用已过时的类、方法时,编译器将会给出警告。如:

@Deprecated
public class Banana {
    @Deprecated
    public void info(){
        System.out.println("我是香蕉");
    }
    public static void  main(String[] args) {
        Banana banana = new Banana();//可以正常调用
        banana.info();//我是香蕉  可以正常运行
    }
}

@deprecated 的作用于文档注释中的 @deprecated 的标记的作用基本相同。
只不过前者需要是 JDK5才支持的注解且修饰程序中的程序单元,如方法、类、接口等,后者需要放在/***/里面

抑制编译器警告:@SuppressWarnings

@SuppressWarnings 指示被该 Annotation 修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告。@SuppressWarnings会一直作用于该程序元素的所有子元素。

public class Vegetables {

    @SuppressWarnings({ "rawtypes", "unused" })
    public static void main(String[] args){
        ArrayList vegetables = new ArrayList();
    }
}

Java 7 的 "堆污染" 警告与@SafeVarargs

在泛型擦除时,可能会引发问题:

public class Sports {
    public static void main(String[] args){
        List list = new ArrayList<Integer>();
        list.add(123);//引发  unchecked 警告
        List<String> list2 = list;//引发未经检查的转换警告 
        System.out.println(list2.get(0));//抛出异常
    }
}

Java 把引发这种错误的原因称为 “堆污染”(Heap pollution),当把一个不带泛型的对象赋给一个带泛型的变量时,往往就会发生这种“堆污染”。

public class ErrorUtilsTest {
    public static void main(String[] args){
        // 发出警告
        ErrorUtils.getInstance().faultyMethod(Arrays.asList("Hello!"),Arrays.asList("World!"));
    }
}
class ErrorUtils{
    private ErrorUtils(){};
    private static ErrorUtils instance = new ErrorUtils();
    public static ErrorUtils getInstance(){
        return instance;
    }
    public void faultyMethod(List<String>...listStrArray){
        // Java 语言不允许创建泛型数组,因此 listArray 只能被当成 List[] 处理
        // 此时相当于把 List<String>赋给了 List,已经发生了“堆污染”
        List[] listArray = listStrArray;
        List<Integer> myList = new ArrayList<Integer>();
        myList.add(new Random().nextInt(100));
        // 把 listArray 的第一个元素赋为 myArray
        listArray[0] = myList;
        String string = listStrArray[0].get(0);
    }
}

此时可以使用 :

Java 8 的函数式接口与 @FunctionalInterface

Java 8 规定:如果接口中只有一个抽象方法(可以包含多个默认方法或多个 static 方法),该接口就是函数式接口。@FunctionalInterface 就是用来指定某个接口必须是函数式接口。

函数式接口就是为 Java 8 的 Lambda 表达式准备的, Java 8 允许使用 Lambda 表达式创建函数式接口的实例。因此 Java 8 专门增加了 @FunctionalInterface

例如:

@FunctionalInterface
public interface FunInterface {
    static void foo(){
        System.out.println("foo 类方法");
    }
    default void bar(){
        System.out.println("bar 默认方法");
    }
    void test();//只定义一个抽象方法
    //void test2(); 定义第二个方法将报错
}

@FunInterface 还能修饰接口,不能修饰其他程序元素。

初始化块

初始化块

Java 使用构造器来对单个对象进行初始化操作,使用构造器先完成整个 Java 对象的状态初始化,然后将 Java 对象返回给程序,从而让该 Java 对象的信息更加完整。与构造器作用非常类似的是初始化块,它也可以对Java对象进行初始化操作。

使用初始化块

初始化块是 Java 类里可出现的第4中成员(成员变量、方法和构造器),一个类里可以有多个初始化块,相同类型的初始化块之间有顺序:前面定义的初始化块先执行,后面定义的初始化块后执行。初始化模板:

[修饰符]{
    //初始化块的可执行代码
    ……
}

初始化块的修饰符只能是 static (不然就不加修饰符),使用了 static 修饰的初始化块被称为静态初始化块。初始化块里的代码可以包含任何可执行性语句,包括定义局部变量、调用其他对象的方法,以及使用分支、循环等语句等。

public class Demo8 {
    {
        int a = 1;
    }
}

初始化块虽然也是 Java 类的一种成员,但它没有”名字“,也就没有表示,因此无法通过类、对象来调用初始化块。初始化块只在创建 Java 对象时隐式执行,而且在执行构造器之前执行。

虽然 Java 允许在一个类里面定义 2 个普通初始化块,但是这没有任何意义。因为初始化块是在创建 Java 对象时隐式执行的,而且它们总会全部执行,因此完全可以把多个普通初始化块合并成一个初始化块没从而可以让程序更加简洁,可读性强。

  • 当 Java 创建一个对象时,系统先为该对象的所有实例变量分配内存(前提是该类已经被加载过了),接着程序开始对这些实例变量执行初始化,其初始化顺序是:先执行初始化块或声明实例变量时制定的初始值(这两个地方指定初始值的执行允许与它们在源代码中的排列顺序相同),再执行构造器里制定的初始值。

初始化块和构造器

从某种程序上来看,初始化块是构造器的补充,初始化块总是在构造器执行之前执行。系统同样可使用初始化块来进行对象的初始化操作。

与构造器不同的是,初始化块是一段固定执行的代码,它不能接受任何参数,因此初始化块对同一个类的所有对象所进行的初始化处理完全相同。基于这个原因,不难发现初始化块的基本用法,如果有一段初始化块处理代码对所有对象完全相同,且无须接受任何参数,就可以把这段初始化处理代码提取到初始化块中。

实际上,初始化块只是一个假象,使用 javac 命令编译 Java 类后,该 Java 类中的初始化块会消失 —— 初始化块中代码会被“还原“ 到每个构造器中,且位于构造器所有代码的前面。

静态初始化块

如果定义初始化块时使用了 static 修饰符,则这个初始化块就变成了静态初始化块,也被称之为类初始化块(普通初始化块负责对对象执行初始化,类初始化块则负责对类进行初始化)。静态初始化块是泪相关的,系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行。因此静态初始化块总是比普通初始化块先执行。

静态初始化时类相关的,用于对整个类进行初始化处理,通常用于对类变量执行初始化处理。静态初始化块不能对实例变量进行初始化处理。

  • 静态初始化也被称为 类初始化块,也属于类的静态成员,同样需要遵循静态成员不能访问非静态成员的规则,因此静态初始化块不能访问非静态成员,包括还不能访问实例变量和实例方法。

认识老朋友-类成员

认识老朋友-类成员

  • static 关键字修饰的成员就是类成员,static 关键字不能修饰构造器。static 修饰的类成员属于整个类,不属于单个实例。

理解类成员

在 Java 类里只能包含成员变量、方法、构造器、初始化块、内部类(包括接口、枚举) 5 种成员,其中 static 修饰的成员就是累成员。类成员属于整个类,而不属于单个对象。

类变量属于整个类,当系统第一次准备使用该类时,系统会为该类变量分配内存空间,类变量开始生效,直到该类被卸载,该类的类变量所占用的内存才会被系统的垃圾回收机制回收。类变量生存范围几乎等同于该类的生存范围。当类初始化完成后,类变量也被初始化完成。

类变量既可通过类来访问,也可以通过类的对象来访问。但通过类的对象来访问类变量时,实际上并不是访问该对象所拥有的变量,因为当系统创建该类的对象时,系统不会再为类变量分配内存,也不会再次对类变量进行初始化,也就是说,对象根本不拥有对应类的类变量,通过对象访问类变量只是一种假象,通过对象访问的依然是该类的类变量,可以这样理解:当通过对象来访问类变量时,系统会在底层转换为该类来访问类变量。

很多语言都不允许通过对象访问类变量,对象只能访问实例变量;但类变量必须通过类来访问。

由于对象实际上并不持有类变量,类变量是由该类持有的,同一个类的所有对象访问类变量时,实际上访问的都是该类所持有的变量。因此,从程序运行表面来看,即可看到同一类的所有实例的类变量共享同一块内存区。

类方法也是类成员的一种,类方法也是属于类的,通常直接使用类作为调用者来调用类方法,但也可以使用对象来调用类方法。与类变量类似,即使使用对象来调用类方法,其效果也与采用类来调用类方法完全一样。

当使用实例来访问类变量时,实际上依然是委托给该类来访问类成员,因此即使某个实例为 null ,它也可以访问它所属类的类成员。

如果一个 null 对象访问实例成员(包括实例变量和实例方法),将会引发 NullPointerException 异常,因为 null 表明该实例根本不存在,既然实例不存在,那么它的实例变量和实例方法自然也不存在。

静态初始化块也是类成员的一种,静态初始化块用于执行类初始化动作,在类的初始化阶段,系统会调用该类的静态初始化块来对类进行初始化。一旦该类初始化结束后,静态初始化将不会获得执行的机会。

  • 对 static 关键字而言,有一条非常重要的规则:类成员(包括方法、初始化块、内部类和枚举类)不能访问实例成员(包括成员变量、方法、初始化块、内部类和枚举类)。因为类成员是属于类的,但实例成员还不曾初始化的情况,如果允许类成员访问实例成员将会引起大量错误。

单例(Singleton)类

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

根据良好的封装的原则:一旦把该类的构造器隐藏起来,就需要提供一个 public 方法作为该类的访问点,用于创建该类的对象,且该方法必须使用 static 修饰(因为调用该方法之前还不存在对象,因此使用该方法的不可能是对象,只能是类)。

除此之外,该类还必须缓存已经创建的对象,否则该类无法知道是否曾经创建过对象,也就无法保证值创建一个对象。为此该类需要使用一个成员变量来保存曾经创建的对象,因为该成员变量需要被上面的静态方法访问,故该成员变量必须使用 static 修饰。

public class Singleton {

    private static Singleton instance;
    static{};
    public static Singleton getInstance(){
        if(instance==null){
            instance = new Singleton();
        }
        return instance;
    }
}

JAVA 集合

JAVA 集合

  • Java 集合类主要由两个接口派生而出: Collection 和 Map , Collection 和 Map 是Java 集合框架的根接口,这两个接口又包含了一些子接口或实现类:

Collection 接口、子接口及其实现类的继承树:
qq 20160326220150

Map 体系的继承树
qq 20160326223255

Collection 和 Iterator 接口

Collection 接口是 List、Set 和 Queue 接口的父接口,Collection接口里定义了如下操作集合元素的方法:

  • boolean add(Object o):添加一个元素。
  • boolean addAll(Collection c):将集合 c 里的所有元素添加到指定集合里。
  • void clear():清楚集合里的所有元素,将元素长度变为0。
  • boolean contains(Object o):返回集合里是否包含指定元素。
  • boolean containsAll(Collection c):返回集合里是否包含集合 c 里的所有元素
  • boolean isEmpty():返回集合是否为空。根据长度是否为0。
  • Iterator iterator():返回一个 Iterator 对象,用于遍历集合里的元素。
  • boolean remove(Object o):删除集合中的指定元素 o。
  • boolean removeAll(Collection c):从集合中删除集合 c 里包含的所有元素。删除一个或一个以上返回true。
  • boolean retainAll(Collection c):从集合中删除集合 c 里不包含的元素
  • int size():返回集合里元素的个数。
  • Object[] toArray():将集合转换成一个数组。
public class CollectionDemo {
    public static void main(String[] args){
        Collection collection = new ArrayList<>();
        //添加元素
        collection.add("醉流觞");
        //虽然集合不能存放基本类型的值,但是Java支持自动装箱
        collection.add(5);
        System.out.println(collection.size());//2
        //删除指定的元素
        collection.remove(5);
        System.out.println(collection.size());//1
        //判断是否包含字符串
        System.out.println("判断是否包含字符串:"+collection.contains("醉流觞"));//true
    }
}

集合类就像容器,现实生活中容器的功能,无非就是添加对象,删除对象、清空容器、判断容器是否为空等,集合类就为这些功能提供了对应的方法。具体方法可查看Java API 文档。

在传送模式下,把一个对象“丢进”集合中后,集合会忘记这个对象的类型 —— 也就是说,系统把所有的集合元素都当成 Object 类型。但是,我们可以使用泛型来限制集合里元素的类型,并让集合记住所有集合元素的类型。

使用 Java 8 增强的 Iterator 遍历集合元素。

Iterator 接口隐藏了各种 Collection 实现类的底层细节,向应用程序提供了遍历 Collection 集合元素的同一编程接口。Iterator 接口里定义了如下方法:

  • boolean hasNext():如果被迭代的集合元素还没有被遍历完,返回true。
  • Object next():返回集合里的下一个元素。
  • void remove():删除集合里上一次 next 方法返回的元素。
  • void forEachRemaining(Consunmer action):这是 Java 8 位 Iterator 新增的默认方法,该方法可使用 Lambda表达式来遍历几何元素。

示例代码:

public class IteratorDemo {
    public static void main(String[] args){
        //创建集合、添加元素
        Collection collection = new ArrayList<>();
        collection.add("醉流觞");
        collection.add("raindrops");
        collection.add("zuiliushang");
        //获取collection集合对应的迭代器
        Iterator iterator = collection.iterator();
        while (iterator.hasNext()) {
            String string = (String) iterator.next();
            System.out.println(string);
            if (string.equals("zuiliushang")) {
                iterator.remove();
            }
            string="啊啊";
        }
        System.out.println(collection);//[醉流觞, raindrops]
    }
}

使用 Lambda 表达式遍历集合。

  • Java 8 位 Iterable 接口新增了一个 forEach(Consumer action) 默认方法,该方法所需参数的类型是一个函数式接口,而 Iterable 接口是 Collection 接口的父接口,因此 Collection 集合也可直接调用该方法。

当程序调用 Iterable 的 forEach(Consumer action) 遍历集合元素时,程序会一次将集合元素传给Consumer 的 accept(T t) 方法(该接口中唯一的抽象方法)。正因为 Consumer 是函数式接口,因此可以使用 Lambda 表达式来遍历集合元素。

例子:

public class CollectionEach {
    public static void main(String[] args){
        //创建一个集合
        Collection books = new HashSet<>();
        books.add("book1");
        books.add("raindrops");
        books.add("zuiliushang");
        //调用 forEach() 方法遍历集合
        books.forEach(obj->System.out.println("迭代集合元素:"+obj));
    }
}

使用 foreach 循环遍历集合元素

  • Java 5 提供的 foreach 循环迭代访问集合元素更加便捷:
public class ForeachDemo {
    public static void main(String[] args){
        // 创建集合、添加元素
        Collection<String> collection = new ArrayList<>();
        collection.add("醉流觞");
        collection.add("raindrops");
        collection.add("zuiliushang");
        for (String item : collection) {
            System.out.println(item);
            /*if (item.equals("zuiliushang")) {
                collection.remove(item);//java.util.ConcurrentModificationException
            }*/
        }
    }
}

instanceof 运算符

instanceof 运算符

instanceof 运算符的前一个操作通常是一个引用类型变量,厚一个操作数通常是一个类(也可以是接口),用于判断前面的对象是否是后面的类,或者是其子类、实例类的实例。如果是,返回 true,否则返回false。

注意:instanceof 运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误。

public class Demo6 {
    public static void main(String args[]){
        //声明hello时使用 Object 类,则 hello 的编译类型是 Object
        //Object 是所有类的父类,但 hello 变量的实际类型是 String
        Object hello = "object";
        //String 与 Object 存在继承关系,可以进行 instanceof 运算。返回 true
        System.out.println("字符串是否是 Object 类的实例:"+(hello instanceof Object));
        System.out.println("字符串是否是 String 类的实例:"+(hello instanceof Object));
    }
}

国际化和格式化

国际化

Java 国际化的思路

  • Java 程序的国际化思路是将程序中的标签、提示灯信息放在资源文件中,程序需要支持哪些国家、语言环境,就对应提供相应的资源文件。资源文件是 key-value 对,每个资源文件中的 key 是不变的,但 value 则随着不同的国家、语言而改变。

Java 程序的国际化主要通过如下三个类来完成:

  • java.util.ResourceBundle:用于加载国家、语言资源包。
  • java.util.Locale:用于封装特定的国家/区域、语言环境。
  • java.text.MessageFormat:用于格式化带占位符的字符串。

为了实现国际化,必须先提供程序锁需要的资源文件。资源文件命名可以有一下形式:

  • baseName_language_country.properties
  • baseName_language.properties
  • baseName.properties

其中 baseName 是资源文件的基本名,可以随意指定;而 language 和 country 不可随意变化,必须是Java 所支持的语言和国家

Java 支持的国家和语言。

  • 要获取 Java 所支持的国家和语言,可调用 Locale 类的 getAvailableLocales() 方法,该方法返回一个 Locale 数组。
public class LocaleTest {
    public static void main(String[] args){
        //返回Java 所支持的全部国家和语言的数组
        Locale[] localeList = Locale.getAvailableLocales();
        //遍历数组的每个元素,依次获取所支持的国家和语言
        for (int i = 0; i < localeList.length; i++) {
            System.out.println(localeList[i].getDisplayCountry()+""
                    + "="+localeList[i].getCountry()+" "
                    + ""+localeList[i].getDisplayLanguage()+""
                            + "="+localeList[i].getLanguage());
        }
    }
}

继承与组合

继承与组合

子类扩展父类时,子类可以从父类继承得到成员变量和方法,如果访问权限允许,子类可以直接访问父类的成员变量和方法,相当于子类可以直接服用父类的成员变量和方法,确实非常方便。

但继承带来了高度复用的同时,也带来了一个严重的问题:继承严重地破坏了父类的封装性。因为:

每个类都应该封装它内部信息和实现细节了,而值暴露必要的方法给其他类使用。但在继承关系中,子类可以直接访问父类的成员变量(内部信息)和方法,从而造成子类和父类的严重耦合。

这导致了:父类的实现细节对子类不再透明,子类可以访问父类的成员变量和方法,并可以改变父类方法的实现细节(如通过方法重写的方式来改变父类的方法实现),从而导致了子类可以而已篡改父类的方法。

所以,为了保证父类具有良好的封装性,不会被子类随意改变,设计父类通常应遵循如下规则:

  • 尽量隐藏父类的内部数据。尽量把父类的所有成员变量都设置成 private 访问类型,不要让子类直接访问父类的成员变量。
  • 不要让子类可以随意访问、修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用 private 访问控制符修饰,让子类无法访问该方法;如果父类中的方法需要被外部类调用,则必须以public 修饰,但又不希望子类重写该方法,可以使用 final 修饰符来修饰该方法;如果父类的某个方法被子类重写,但不希望被其他类自有访问,则可以使用protected 来修饰该方法。
  • 尽量不要在父类构造器中调用将要被子类重写的方法。

而当我们需要从父类派生新的子类时,不仅要保证子类时一种特殊的父类,而且需要具备以下两个条件之一:

  • 子类需要额外增加属性,而不仅仅是属性值的改变。
  • 子类需要增加自己独有的行为方式(包括增加新的方法或者重写父类的方法)。

利用组合实现复用

  • 如果只是出于类复用的目的,并不一定需要使用继承,完全可以使用组合来实现。

如果需要复用一个类,除了把这个类当成基类来继承之外,还可以把该类当成另一个类的组成成分,从而允许心累直接服用该类的 public 方法。不管是继承还是组合,都允许在新类(对于继承就是子类)中直接复用旧类的方法。

对于继承而言,子类可以直接获得父类的 public 方法,程序使用子类时,将可以直接访问该子类从父类那里继承到的方法;而组合则是把旧类对象作为新类的成员变量组合进来,用以实现新类的功能,用户看到的是新类的方法,而不能看到被组合对象的方法,因此,通常需要在新类里使用 private 修饰被组合的旧类对象。

class Animal{
    private void eat(){
        System.out.println("大口吃饭");
    }
    public void eaten(){
        eat();
        System.out.println("好好吃");
    }
}

class Bear{
    private Animal animal;
    public Bear(Animal animal){
        this.animal = animal;
    }
    public void eaten(){
        animal.eaten();//直接调用 animal中的方法
    }
    public void run(){
        System.out.println("跑跑跑~~");
    }
}

class Tiger{
    private Animal animal;
    public Tiger(Animal animal){
        this.animal = animal ;
    }
    public void eaten(){
        animal.eaten();
    }
    public void jump(){
        System.out.println("跳跳跳~~");
    }
}
public class Demo7 {
    public static void main(String args[]){
        Animal animal = new Animal();
        Tiger tiger = new Tiger(animal);
        tiger.eaten();
        tiger.jump();

        Animal animal2 = new Animal();
        Bear bear = new Bear(animal2);
        bear.eaten();
        bear.run();
    }
}

大部分时候,继承关系中从多个子类里抽象出共有父类的过程,类似于组合关系总从多个整体类型提取被组合类的过程:继承关系中从父类派生子类的过程,则类似于组合关系中把被组合类组合到整体类的过程。

JDK 的 Meta Annotation(元 Annotation)

JDK 的 Meta Annotation(元 Annotation)

JDK 除了在 java.lang 下提供了 5个基本的 Annotation 之外,还在 java.lang.annotation 包下提供了 6 个 Meta Annotation(元 Annotation),其中有 5 个元 Annotation 都用于修饰其他的 Annotation 定义。其中 @repeatable 专门用于定义 Java 8 新增的重复注解,后面补充,这里先说明4个元 Annotation。

使用@retention

@retention 只能用于修饰 Annotation 定义,用于指定被修饰的 Annotaion 可以保留多长时间@retention 包含一个 RetentionPolicy 类型的 value 成员变量, 所以使用 @retention 时必须为该 value 成员变量指定值。

value 成员变量的值只能是如下三个:

  • RetentionPolicy.CLASS:编译器将把 Annotation 记录在 class 文件中。当运行 Java 程序时,JVM不可获取 Annotation 信息。这是默认值。
  • RetentionPolicy.RUNTIME:编译器将把 Annotation 记录在 class 文件中。当运行 Java 程序时,JVM 也可获取 Annotation 信息,程序可以通过反射获取该 Annotation 信息
  • RetentionPolicy.SOURCE: Annotation 只保留在源代码中,编译器直接丢弃这种 Annotation。

如果需要通过反射获取注解信息,就需要使用 value 属性值为 RetentionPolicy.RUNTIME 的 @retention。使用 @retention 元 Annotation 可采用如下代码为 value指定值。

    // 定义下面的 Testable Annotation 保留到运行时
    @Retention(value = RetentionPolicy.RUNTIME)
    public @interface Testable {
    }

也可采用如下代码来为 value 指定值:

    @Retention(RetentionPolicy.SOURCE)
    public @interface Testable{
    }

上面代码中使用 @retention 元 Annotation 时,并未通过 value=RetentionPolicy.SOURCE 的方式来为该成员变量指定值,这是因为当 Annotation 的成员变量名为 value 时,程序中可以直接在 Annotation 后的括号里指定该成员变量的值,无须使用 name=value 的形式。

如果使用注解时只需要为 value 成员变量指定值,则使用该注解时可以直接在该注解后的括号里指定 value 成员变量的值,无须使用 “ value=变量值 ” 的形式。

使用 @target

@target 也只能修饰一个 Annotation 定义,它用于指定被修饰的 Annotation 能用于修饰哪些程序单元。@target 元 Annotation 也包含一个名为 value 的成员变量,该成员变量的值只能是如下几个:

  • ElementType.ANNOTATION_TYPE:指定该策略的 Annotation 只能修饰 Annotation。
  • ElementType.CONSTRUCTOR:指定该策略的 Annotation 只能修饰构造器。
  • ElementType.FIELD:指定该策略的 Annotation 只能修饰成员变量。
  • ElementType.LOCAL_VARIABLE:指定该策略的 Annotation 只能修饰局部变量。
  • ElementType.METHOD:指定该策略的 Annotation 只能修饰方法定义。
  • ElementType.PACKAGE:指定该策略的 Annotation 只能修饰包含义。
  • ElementType.PARAMETER:指定该策略的 Annotation 可以修饰参数。
  • ElementType.TYPE:指定该策略的 Annotation 可以修饰类、接口(包括注解类型)或枚举定义。

与使用 @retention 类似的是,使用 @target 也可以直接在括号里指定 value 值,而无须使用 name=value 的形式。

使用 @documented

@documented 用于指定被该元 Annotation 修饰的 Annotation 类将被 javadoc 工具提取成文档,如果定义 Annotation 类时使用了 @documented 修饰,则所有使用该 Annotation 修饰的程序元素的 API 文档中将会包含该 Annotation 说明。

使用 @inherited

@inherited 元 Annotation 指定被它修饰的 Annotation 将具有继承性 —— 如果某个类使用了@xxx 注解(定义该 Annotation 时使用了 @inherited 修饰) 修饰,则其子类将自动被@xxx 修饰。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Inheritable {}

上面程序中的粗体字代码表明@Inheritable 具有继承性,如果某个类使用了@Inheritable 修饰,则该类的子类将自动使用@Inheritable 修饰。

@Inheritable
class Base{
}
// InheritableTest 类知识继承了 Base 类
// 并未直接使用 @Inheritable Annotation 修饰
public class InheritableTest extends Base{
    public static void main(String[] args) {
        // 打印 InheritableTest 类是否有 @Inheritable 修饰
        System.out.println(InheritableTest.class.isAnnotationPresent(Inheritable.class));//true
    }
}

文件传输基础——Java IO流

文件传输基础——Java IO流

目录

1. 编码问题

2. File类的使用

3. RandomAccessFile 的使用

4. 字节流的使用

5. 字符流的使用

6. 对象的序列化和反序列化

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

编码问题

  • GBK编码中一个中文字符占两个字节,一个英文字符占一个字节;

  • UTF-8编码中一个中文字符占三个字节,一个英文字符占一个字节;

  • JAV是双字节编码,是UTF-16be编码,一个中文字符占两个字节,一个英文字符占两个字节。

  • 在中文系统上默认的是ansi编码,即GBK编码。

    当字节序列是某种编码时,若把字节序列变成字符串,也需要用这种编码方式,否则会乱码。

  • 例如:

枚举类

枚举类

  • 在某些情况下,一个类的对象是有限而且固定的,比如季节类,它只有4个对象;行星类,有8个对象。这些实例有限而且固定的类,在 Java 里被称为枚举类

枚举类入门

Java 5 新增了一个 enum 关键字(它与 class、interface 关键字的地位相同),用以定义枚举类。枚举类是一个特殊的类,它一样可以有自己的成员变量、方法,可以实现一个或者多个接口,也可以定义自己的构造器。 一个 Java 源文件中最多只能定义一个 public 访问权限的枚举类,且该 Java 源文件也必须和该枚举类的类名相同。

枚举类和普通类的区别:

  • 枚举类可以实现一个或者多个接口,使用 enum 定义的枚举类默认继承了 java.lang.Enum 类,而不是默认继承 Object 类,因此枚举类不能显式继承其他父类。其中 java.lang.Enum 类实现了 java.lang.Serializable 和 java.lang.Comparable 两个接口。
  • 使用 enum 定义、非抽象的枚举类默认会使用 final 修饰,因此枚举类不能派生子类。
  • 枚举类的构造器只能使用 private 访问控制符,如果省略了构造器的访问控制符,则默认使用 private;如果强制指定访问控制符,则只能指定 private 修饰符。
  • 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加 public static final修饰,无须显式添加。

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

public enum Demo10 {
    //在第一行列出4个枚举实例;
    SPRING,SUMMER,FALL,WINTER;
}

调用枚举类:

public class Demo11 {
    public void test(Demo10 e){
        switch (e) {
        case SPRING:
            System.out.println("我是春天Z");
            break;
        case SUMMER:
            System.out.println("我是夏天ZZ");
            break; 
        case FALL:
            System.out.println("我是秋天ZZZ");
            break;
        case WINTER:
            System.out.println("我是冬天ZZZZ");
            break;
        default:
            break;
        }
    }
    public static void main(String[] args){
        for (Demo10 e : Demo10.values()) {
            System.out.println(e);
        }
        new Demo11().test(Demo10.SPRING);
    }
}
  • int compareTo(E o):该方法用于与指定枚举对象比较顺序,同一个枚举实例只能与相同类型的枚举实例进行比较。如果该枚举对象位于指定枚举对象之后,则返回正整数;如果该枚举对象位于指定枚举对象之前,则返回负整数,否则返回 0 。
  • String name():返回此枚举实例的名称,这个名称就是定义枚举类时列出的所有枚举值之一。与此方法相比,大多数情况下应该优先考虑使用 toString() 方法,因为 toString() 方法返回更加友好的名称。
  • int ordinal(): 返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置,第一个枚举值的索引值为零)。
  • String toString(): 放回枚举常量的名称,与 name 方法相似,但更常用。
  • public static <T extends Enum > T valueOf(Class enumType,String name):这是一个静态方法,用于返回指定枚举类中指定名称的枚举值。名称必须与在该枚举类中声明枚举值时所用的标识符完全匹配,不允许使用额外的空白字符。

枚举类的成员变量、方法和构造器。

  • 枚举类也是一种类,因此它可以定义成员变量、方法和构造器:
public enum Gender {
    MALE,FEMALE;
    public String name;
}

使用枚举类:

public class TestGender {
    public static void main(String[] args){
        Gender gender = Enum.valueOf(Gender.class, "FEMALE");
        gender.name="女";
        System.out.println(gender + "和" + gender.name);
    }
}

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

将代码改进:(属性私有)

public enum Gender {
    MALE,FEMALE;
    //私有属性 
    private String name;
    public void setName(String name){
        switch (this) {
        case MALE:
            if (name.equals("男")) {
                this.name = name;
            }
            else {
                System.out.println("错误");
            }
            break;
        case FEMALE:
            if (name.equals("女")) {
                this.name = name;
            }
            else {
                System.out.println("错误");
            }
            break;  
        default:
            break;
        }
    }
    public String getName(){
        return name;
    }
}

为枚举类显式定义带参数的构造器:

public enum Gender {
    MALE("男"),FEMALE("女");
    //public static final Gender MALE = new Gender("男");
    //public static final Gender FEMALE = new Gender("女");
    private final String name;
    private Gender(String name) {
        this.name = name ;
    }
    public String getName(){
        return this.name;
    }
}

实现接口的枚举类

public interface GenderDesc {
    void info();
}

public enum Gender implements GenderDesc{
    MALE("男"){
        @Override
        public void info() {
            //啊啊啊
        }
    },FEMALE("女"){
        @Override
        public void info() {
            //哦哦哦
        }
    }
    ;
    //public static final Gender MALE = new Gender("男");
    //public static final Gender FEMALE = new Gender("女");
    private final String name;
    private Gender(String name) {
        this.name = name ;
    }
    public String getName(){
        return this.name;
    }
}

包含抽象方法的枚举类

public enum Operation {
    PLUS {
        @Override
        public double eval(double x, double y) {
            return x + y;
        }
    },MINUS {
        @Override
        public double eval(double x, double y) {
            return x - y;
        }
    },TIMES {
        @Override
        public double eval(double x, double y) {
            // TODO Auto-generated method stub
            return x * y;
        }
    },DIVIDE {
        @Override
        public double eval(double x, double y) {
            // TODO Auto-generated method stub
            return x / y;
        }
    };
    public abstract double eval(double x,double y);
    public static void main(String[] args){
        System.out.println(Operation.DIVIDE.eval(3, 4));
        System.out.println(Operation.MINUS.eval(3, 4));
        System.out.println(Operation.TIMES.eval(3, 4));
        System.out.println(Operation.PLUS.eval(3, 4));
    }
}

JAVA方法详解

JAVA方法详解

方法是类或对象的行为特征的抽象,方法是类或对象最重要的组成部分。但从功能上来说,方法完全类似与传统结构化程序设计里的函数。但是注意:Java 里的方法不能独立存在,所有的方法都必须定义来类里。 方法在逻辑上要么是类,不然就是对象,无方法一说。

方法的属性

不论是从定义方法的语法来看,还是从方法的功能来看,你会发现方法和函数之间有很多相似性。实际上,方法确实是由传统的函数发展而来的,但是最大的不同是:在结构化编程语言里,方法是“一等公民”,而在Java 语言里 , 类才是“一等公民”,离开了类和对象的方法不能独立存在

因此,如果定义方法,则只能在类体内定义,不能独立定义一个方法。一旦将一个方法定义在某个类中,如果这个方法使用了 static 修饰,则这个方法属于这个类,否则这个方法属于这个类的实例。

Java 语言是静态的。一个类定义完成后,只要不再重新编译这个类文件,该类和该类的对象所拥有的方法是固定的,永远不会改变。

方法的参数传递机制

Java 里的参数传递是由 Java 方法的参数传递机制来控制的, Java 里方法的参数传递方法只有一种:值传递。所谓值传递,就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响。

public class Demo4 {

    public static void swap(int a,int b){
        int tmp = a;
        a = b;
        b = tmp;
        System.out.println("swap 方法里, a 的值是:"+ a + ": b 的值得:" + b ) ;//a=9 b=6
    }


    public static void main(String args[]){
        int a = 6;
        int b = 9;
        swap(a, b);
        System.out.println(" a 的值是:"+ a + ": b 的值得:" + b ) ;//a=6 b=9
    }
}

可以看出, swap() 方法里 a 和 b 的值是9、6,交换结束后,变量 a 和 b 的值依然是6、9,可以得出:main() 方法里的变量 a 和 b ,并不是 swap() 方法里的 a 和 b 。
22

实际上,运行程序的时候,系统分别为 main() 和 swap() 方法分配了共2块栈区。调用swap() 方法时,实际上是在swap() 方法栈中重新产生了两个变量 a、b,并将mian() 方法栈区中的a、b分别赋给 swap()栈区中的a、b。

前面是基本类型的参数传递,而 Java 对于引用类型的参数传递,一样采用的是值传递方式。

public class Demo5 {

    public static void swap(DataWrap dw){
        int tmp = dw.a;
        dw.a=dw.b;
        dw.b=tmp;
        //swap 方法里,a 成员变量的值是:9:b 成员变量的值是:6
        System.out.println("swap 方法里,a 成员变量的值是:"+ dw.a + ":b 成员变量的值是:" +
 dw.b);
    }
    public static void main(String[] args){
        DataWrap dw = new DataWrap();
        dw.a = 6;
        dw.b = 9;
        swap(dw);
        //交换结束后, 方法里,a 成员变量的值是:9:b 成员变量的值是:6
        System.out.println("交换结束后, 方法里,a 成员变量的值是:"+ dw.a + ":b 成员变量的值
是:" + dw.b);
    }
}

class DataWrap{
    int a;
    int b;
}

这种调用方式输出结果都是交换后 a 的值为 9 ,b 的值为 6,可能会认为在 main 中调用 swap 方法传入的 dw 是 dw 对象本身,但是记住,这是错觉:

程序从 main() 方法开始执行,main() 方法开始创建了一个 DataWrap 对象,并定义了一个 dw 引用变量来指向 DataWrap 对象,这是一个与基本类型不同的地方。创建一个对象时,系统内存中有两个对象:
堆内存中保存了对象本身,栈内存中保存了引用该对象的引用变量。接着程序通过引用来操作 DataWrap 对象,把该对象 a、b 两个成员变量分别赋值为 6、9:
23

接下来,main() 方法中开始调用 swap() 方法, main() 方法并未结束,系统会分别为 main() 和 swap() 开辟出两个栈区,用于存放 main() 和 swap() 方法的局部变量。调用 swap() 方法时, dw 变量作为实参传入 swap() 方法,同样采用值传递方式:把 main() 方法里 dw 变量的值赋给 swap() 方法里的 dw 形参,从而完成 swap() 方法的 dw 形参的初始化。值得指出的是,main() 方法中的 dw 是一个引用 (也就是一个指针),它保存了 DataWrap 对象的地址值,即也会引用到堆内存中的 DataWrap 对象:
24

可以看出,这种参数传递方式确实是值传递方式,系统一样赋值了 dw 的副本传入 swap() 方法,但关键在于 dw 只是一个引用变量,所以系统赋值了 dw 变量,但并未赋值DataWrap对象。

当程序在 swap() 方法中操作 dw 形参时,由于 dw 只是一个引用变量,故实际操作的还是堆内存中的 DataWrap 对象。此时,不管是操作 main() 方法里的 dw 变量,还是操作 swap() 方法里的 dw 参数,其实都是操作它们所引用 DataWrap 对象,它们引用的是同一个对象。因此,当 swap() 方法中交换 dw 对象的 a、b 两个成员变量的值也被交换了。

为了证明这个事实我们在 swap() 方法的最后一行添加dw = null ;
输出依然不变。也就是 main() 函数中的 dw 并没有因为 swap中的 dw 的改变而改变:

25

内部类

内部类

大部分的时候,类被定义成一个单独的程序单元。在某些情况下,也会把一个类放在另一个类的内部定义,这个定义在其他类内部的类就被称为内部类(有的地方叫嵌套类),包含内部类的类也被称为外部类(也叫宿主类)。

  • 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
  • 内部类成员可以直接访问外部类的私有数据。
  • 匿名内部类适合用于创建那些仅需要一次使用的类。

从语法来看,定义内部类与定义外部类的语法大致相同,内部类除了需要定义在其他类里面之外,还:

  • 内部类比外部类可以多使用三个修饰符: private 、 protected 、 static —— 外部类不可以使用这三个修饰符。
  • 非静态内部类不能拥有静态成员。

非静态内部类

只要把类放在另一个类内部定义就是定义了一个内部类:

public class Out{
   class In{}
}

静态内部类

如果使用 static 修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象,因此使用 static 修饰的内部类被称为类内部类,有的地方也称为静态内部类。

局部内部类

把一个内部类放在方法里面定义,则这个类就是一个局部内部类 = =

Java 8 改进的匿名内部类

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

匿名类定义格式:

new 实现接口() | 父类构造器(实参列表)
{
    //匿名内部类的类体部分
}

匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或实现一个接口

  • 匿名内部类不能是抽象类,因为系统正在创建匿名内部类时,会立即创建匿名内部类的对象。
  • 匿名内部类不能定义构造器。但可以定义初始化块。

最常用的创建匿名内部类的方式是需要创建某个接口类型的对象:

interface Product {
    public double getPrice();
    public String getName();
}
public class Testasd{
    public void test(Product product){
        System.out.println("Price:"+ product.getPrice() + "name:"+ product.getName());
    }
    public static void main(String[] args){
        Testasd test = new Testasd();
        test.test(new Product() {
            @Override
            public double getPrice() {
                return 14250;
            }
            @Override
            public String getName() {
                return "苹果电脑";
            }
        });
    }
}

简化一下代码:

interface Product {
    public double getPrice();
    public String getName();
}
public class Testasd implements Product{
    public void test(Product product){
        System.out.println("Price:"+ product.getPrice() + "name:"+ product.getName());
    }
    public double getPrice() {
        return 12345;
    }
    @Override
    public String getName() {
        return "苹果电脑";
    }
    public static void main(String[] args){
        Testasd test = new Testasd();
        test.test(test);
    }
}

JAVA正则表达式

JAVA正则表达式

正则表达式是一个强大的字符串处理工具,可以对字符串进行查找、提取、分割、替换等操作。String 类里也提供了如下几个特殊的方法:

  • boolean matches(String regex): 判断该字符串是否匹配指定的正则表达式。
  • String replaceAll(String regex,String replacement):将该字符串中所有匹配 regex 的子串替换成 replacement.
  • String replaceFirst(String regex,String replacement):将该字符串中第一个匹配 regex 的子串替换成 replacement.
  • String[] split(String regex):以 regex 作为分隔符,把该字符串分割成多个子串。

除了上面特殊的方法都依赖于 Java 提供的正则表达式之外, Java 还提供了 PatternMatcher 两个类专门用于提供正则表达式支持。

创建正则表达式

正则表达式所包含的合法字符:

字符 解释
x 字符x( x 可代表任何合法的字符)
\0mnn 八进制数0mnn 所表示的字符
\xhh 十六进制值 0xhh 所表示的字符
\uhhhh 十六进制值 0xhhhh 所表示的 Unicode 字符
\t 进制符('\u0009')
\n 新行(换行)符 ('\u000A')
\r 回车符('\u000D')
\f 换页符('\u000C')
\a 报警(bell)符('\u0007')
\e Escape 符('\u001B')
\cx x 对应的控制符。例如,\cM 匹配 Ctrl-M.x值必须为A~Z或a~z之一

正则表达式中的特殊字符:

特殊字符 说明
$ 匹配一行的结尾。要匹配 $ 字符本身,请使用 \$
^ 匹配一行的开头。要匹配 ^ 字符本身,使用 \^
() 标记子表达式的开始和结束位置。要匹配这些字符,使用 \( 和 \)
[] 用于确定中括号表达式的开始和结束位置。要匹配这些字符……
{} 用于标记前面子表达式的出现频度。……
* 指定前面子表达式可以出现零次或多次。……
+ 指定前面子表达式可以出现一次或多次。……
? 指定前面子代表式可以出现零次或一次。……
. 匹配除换行符 \n 之外的任何单字符。……
\ 转义下一个字符。……
| 指定两项之间任选一项。……
"\u0041\\\\"     // 匹配 A \
"\u0061\t"       // 匹配 a <制表符>
"\\?\\["             // 匹配 ?[

Java 字符创中反斜杠本身需要转义,因此两个反斜杠 () 实际上相当于一个 ( 前一个用于转义 )。

上面例子只能匹配单个字符,这是因为还未在正则表达式中使用 “通配符” , “通配符” 是可以匹配多个字符的特殊字符。正则表达式中的 “通配符” 远远超出了普通通配符的功能,它被称为预定义字符:

预定义字符 说明
. 可以匹配任何字符
\d 匹配 0~9 的所有数字
\D 匹配非数字
\s 匹配所有的空白字符,包括空格、制表符、回车符、换页符、换行符等
\S 匹配所有的非空白字符。
\w 匹配所有的单词字符,包括 0~9 所有数字、26个英文字母和下划线 (_)
\W 匹配所有的非单词字符

d 是 digit 代表数字; s 代表 space 代表空白; w 是 word 代表单词;

c\\wt // 可以匹配 cat、cbt、cct、c0t 等等等字符。
\\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d //匹配如 000-000-0000 形式的电话号码。

是不是觉得很强大!!嘿嘿嘿(费玉污脸)。但是如果我们想匹配除了ab 之外的小写字母,或者匹配中文字符,OH MY GOD 怎么办? 没事 ,我们有方括号表达式:

方括号表达式 说明
表示枚举 例如[abc],表示a、b、c其中任意一个字符;[gz]。表示 g、z 其中任意一个字符
表示范围:- 例如[a-f],表示 a~f 范围内的任意字符;[\\u004-\\u0056],表示十六进制字符\u0041 到
\u0056 范围的字符。范围可以和枚举结合使用,如[a-cx-z],表示 a~c、x~z 范围内的任意字符
表示求否:^ 例如[^abc],表示非 a、b、c 的任意字符; [^a-f],表示不是 a~f 范围内的任意字符。
表示“与”运算: && 例如[a-z&&[def]],求a~z和[def]的交集。表示 d、e 或 f
[a-z&&[^bc]],a~z 范围内的所有字符,除了 b 和 c 之外,即[ad-z]
[a-z&&[^m-p]],a~z 范围内的所有字符,除了 m~p 范围之外的字符,即[a-lq-z]
表示“并”运算 并运算与前面的枚举类似。例如[a-d[m-p]],表示[a-dm-p]
  • 正则表达式还支持圆括号表达式,用于将多个代表式组成一个子代表式,圆括号中可以使用或运算符
    (|)。例如,正则表达式((public)|(protected)|(private)) 用于匹配 Java 的三个访问控制符其中之一。

Java 正则表达式还支持如下的边界匹配符:

边界匹配符 说明
^ 行的开头
$ 行的结尾
\b 单词的边界
\B 非单词的边界
\A 输入的开头
\G 前一个匹配的结尾
\Z 输入的结尾,仅用于最后的结束符。
\z 输入的结尾

当我们要建立一个匹配 000-000-0000 形式的电话号码时,使用了 \d\d\d-\d\d\d-\d\d\d\d 正则表达式,这看起来比较繁琐。实际上,正则表达式还提供了数量标识符,正则表达式支持的数量标识符有如下几种模式:

  • Greedy (贪婪模式):数量标识符默认采用贪婪模式,除非另有表示。贪婪模式的表达式会一直匹配下去,知道无法匹配为止。
  • Reluctant (勉强模式):用问号后缀 (?) 表示,它指挥匹配最少的字符。也称为最小匹配模式。
  • Possessive (占有模式):用加号后缀 (+) 表示,目前只有 Java 支持占有模式,通常比较少用。

三种模式的数量表示符:

贪婪模式 勉强模式 占用模式 说明
X? X?? X?+ X 表达式出现零次或一次
X* X*? X*+ X 表达式出现零次或多次
X+ X+? X++ X 表达式出现一次或多次
X{n} X{n}? X{n}+ X 表达式出现 n 次
X{n,} X{n,}? X{n,}+ X 表达式最少出现 n 次
X{n,m} X{n,m}? X{n,m}+ X 表达式最少出现 n 次,最多出现 m 次
  • 贪婪模式和勉强模式的对比:
public class GreedyReluctant {
    public static void main(String[] args) {
        String string = "hello,java!";
        //贪婪模式下的正则表达式。
        System.out.println(string.replaceFirst("\\w*", "●")); //●,java!
        //勉强模式下的正则表达式。
        System.out.println(string.replaceFirst("\\w*?", "℃"));//℃hello,java!
    }
}

自定义 Annotation

自定义 Annotation

定义 Annotation

定义新的 Annotation 类型使用@interface 关键字(在原有的 interface 关键字前增加@符号)定义一个新的 Annotation 类型与定义一个接口非常像,如:

// 定义一个简单的 Annotation 类型
public @interface Test {
}

定义了该 Annotation 之后,就可以在程序的任何地方使用该 Annotation,使用 Annotation的语法类似于 public 、 final 修饰符。但是通常会把 Annotation 放为一行, 因为可能命名会太长:

@Test
public class MyClass {
    @Test
    public void info(){

    }
}

Annotation 不仅可以是上面这种简单的 Annotation,还可以带成员变量, Annotation 的成员变量在 Annotation 定义中以无形参的方法形式来声明,其方法名和返回值定义了该成员变量的名字和类型。
如:

public @interface MyTag {
    //定义带两个成员变量的 Annotation
    //Annotation 中的成员变量以方法的形式来定义
    String name();
    int age();
}

这种定义方法是不是有种定义接口的既视感呢?只是 Annotation 用的是 @interface 关键字来定义,interface 用的是 interface 关键字来定义

》》》》》》》》》

实际上 使用@interface 定义的 Annotation 的确非常像定义了一个注解接口,这个注解接口继承了 Annotation 接口,这一点可以通过反射看到 MyTag 接口里包含了 Annotation 接口里的方法。

现在让我们来使用成员变量:

@Test
@MyTag(age = 20, name = "raindrops")//成员变量不能省略
public class MyClass {
    @Test
    public void info(){

    }
}
  • 当然咯,用脚想都知道可以在定义成员变量给予一个默认值:
public @interface MyTag {
    //定义带两个成员变量的 Annotation
    //Annotation 中的成员变量以方法的形式来定义
    String name() default "raindrops";
    int age() default 20;
}
``
这次因为有默认值,使用的时候就可以省略。
```java
@Test
@MyTag//成员变量省略
public class MyClass {
    @Test
    public void info(){

    }
}

根据 Annotation 是否可以包含成员变量,可以把 Annotation 分为如下两类:

  • 标记 Annotation:没有定义成员变量的 Annotation 类型被称为标记。这种 Annotation 仅利用自身的存在与否来提供信息,如@OverRide@test 等 Annotation。
  • 元数据 Annotation:包含成员变量的 Annotation , 因为它们可以接受更多的元数据,所以也被称为元数据 Annotation (Meta Annotation)。

提取 Annotation 信息

使用 Annotation 修饰了类、方法、成员变量等成员之后,这些 Annotation 不会自己生效,必须由开发者提供相应的工具来提取并处理 Annotation 信息。

Java 使用 Annotation 接口来代表成员元素前面的注解,该接口是所有注解的父接口。 Java 5 在 java.lang.reflect 包下新增了 AnnotatedElement 接口,该接口代表程序中可以接受注解的程序元素。该接口主要有如下几个实现类:

  • Class:类定义。
  • Constructor:构造器定义。
  • Field:类的成员变量定义。
  • Method:类的方法定义。
  • Package:类的包定义。

java.lang.reflect 包下主要包含一些实现反射功能的工具类,从 Java5 开始, java.lang.reflect 包所提供的反射 API 增加了读取运行时 Annotation 的能力。只有当定义 Annotation 时使用了 @retention(RetentionPolicy.RUNTIME)修饰,该 Annotation 才会在运行时课件,JVM 才会在装载 *.class 文件时读取保存在 class 文件中的 Annotation。

AnnotatedElement 接口是所有成员元素(如Class、Method、Constructor 等)的父接口,所以程序通过反射获取了某个类的 AnnotatedElement 对象(如 Class、Method、Constructor 等)之后,程序就可以调用该对象的如下几个方法来访问 Annotation 信息:

  • A getAnnotation(Class annotationClass):返回该程序元素上存在的、指定类型的注解,如果该类型的注解不存在,则返回 null。
  • A getDeclaredAnnotation(Class annotationClass):这是 Java 8 新增的方法,该方法尝试获取直接修饰该程序元素、指定类型的 Annotation。如果该类型的注解不存在,则返回 null。
  • Annotation[] getAnnotations():返回该程序元素上存在的所有注解
  • Annotation[] getDeclaredAnnotation():返回直接修饰该程序元素的所有 Annotation 。
  • boolean isAnnotationPresent(Class<? extends Annotation>annotationClass):判断该程序元素上是否存在指定类型的注解,如果存在则返回 true ,否则返回 false。
  • A[] getAnnotationByType(ClassannotationClass):该方法的功能与前面介绍的 getAnnotation() 方法基本相似。但由于 Java8 增加了重复注解功能,因此需要使用该方法获取修饰该程序元素、指定类型的多个 Annotation。
  • A[] getDeclaredAnnotationsByType(Class annotationClass):该方法的功能与前面介绍的 getDeclaredAnnotations() 方法基本相似。但由于 Java8 增加了重复注解功能,因此需要使用该方法获取直接修饰该程序元素、指定类型的多个 Annotation。

为了获得程序中的程序元素 (如 Class、Method 等),必须使用反射知识。

@Testable//自定义注解
@Test//自定义注解 
@MyTag(age = 20, name = "raindrops")//自定义注解 
public class MyClass {
    @org.junit.Test//Junit的注解 
    @com.zuiliushang.annotationdiy.Test//自定义注解
    @MyTag(age = 20, name = "raindrops")//自定义注解
    public void info(){ 
    }
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException{
        //获取 MyClass类的info 方法的所有注解
        Annotation[] annotations = Class.forName("com.zuiliushang.annotationdiy.MyClass").getMethod("info").getAnnotations();
        //遍历
        System.out.println(annotations.length);
        for (Annotation annotation : annotations) {
            System.out.println(annotation);
        }
        //System.out.println(Class.forName("com.zuiliushang.annotationdiy.MyClass"));
        //用对象来获取注解
        MyClass myClass = new MyClass();
        Annotation[] annotations1 = myClass.getClass().getDeclaredAnnotations();
        System.out.println(annotations1.length);
        for (Annotation annotation : annotations1) {
            System.out.println(annotation);
                if (annotation instanceof MyTag) {
                System.out.println("annotation.name():"+ ((MyTag)annotation).name());
                System.out.println("annotation.age():"+ ((MyTag)annotation).age());
            }
        }
    }
}

/*
输出结果
3
@org.junit.Test(timeout=0, expected=class org.junit.Test$None)
@com.zuiliushang.annotationdiy.Test()
@com.zuiliushang.annotationdiy.MyTag(name=raindrops, age=20)
3
@org.junit.Test(timeout=0, expected=class org.junit.Test$None)
@com.zuiliushang.annotationdiy.Test()
@com.zuiliushang.annotationdiy.MyTag(name=raindrops, age=20)
annotation.name():raindrops
annotation.age():20
*/

需要注意的是,自定义 Annotation 的时候,一定要记住设置 @retention 不然可是提取不到的哦

使用 Annotation 的实例(学完反射回来填坑):

自定义一个 Annotation:

Java 8 新增的重复注解

在 Java8 以前,同一个程序元素前最多只能使用一个相同类型的 Annotation;如果需要在同一个元素前使用多个相同类型的 Annotation ,则必须使用 Annotation “容器”。例如在 Struts2 开发中,有时需要在 Action 类上使用多个 @Result 注解。在 Java8 以前只能写成如下形式:

    @Results({@Result(name="falure",location="failed.jsp"),
    @Result(name="sucess",location="success.jsp")})
    public Action FooAction{...}

上面代码使用了两个 @Result 注解,但由于传统 Java 语法不允许多次使用 @Result 修饰同一个类,因此程序必须使用 @results 注解作为两个 @Result 的容器 —— 实质是,@results 注解只包含一个名字为 value、类型为 Result[] 的成员变量,程序指定的多个 @Result 将作为 @results 的 value 属性(数组类型)的数组元素。

从 Java8 开始,上面语法可以得到简化,因此上面代码可能可简化为:

    @Result(name="falure",location="failed.jsp")
    @Result(name="sucess",location="success.jsp")
    public Action FooAction{...}

下面是一个基本的 Annotation:

//指定该注解信息会保留到运行时
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface J8AnnDemo {
    // 为该注解定义2个成员变量
    String name() default "raindrops";
    int age();
}

多个这个注解如果修饰同一个类,编译器会报错。

为了将该注解改造成重复注解,需要使用 @repeatable 修饰该注解,使用@repeatable 时必须为 value 成员变量指定值,该成员变量的值应该是一个“容器” 注解 —— 该 “容器” 注解可包含多个 @J8AnnDemo,因此还需要定义如下的 “容器” 注解。

//指定该注解信息会保留到运行时
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface J8AnnDemos {
    //定义 value 成员变量,该成员变量可接受多个 @J8AnnDemo 注解
    J8AnnDemo[] value();
}

该代码中的 J8AnnDemo[] value(); 定义了一个 J8AnnDemo[] 类型的 value 成员变量,意味着 @J8AnnDemos 注解的 value 成员变量可接受多个 @J8AnnDemo 注解, 因此@J8AnnDemos 注解可作为 @J8AnnDemo 的容器。

容器注解的保留期必须必它所包含的注解的保留期更长,否则编译器会报错

接下来就可以在定义@J8AnnDemo 注解时候添加 @repeatable 注解修饰。
例:

//指定该注解信息会保留到运行时
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(J8AnnDemos.class)
public @interface J8AnnDemo {
    // 为该注解定义2个成员变量
    String name() default "raindrops";
    int age();
}

现在可以试试了,多个这个注解如果修饰同一个类,不会报错

实际上,@J8AnnDemo(age = 0)@J8AnnDemo(age = 0) 只是一种简化写法而已。

/*@J8AnnDemo(age = 0)
@J8AnnDemo(age = 2)*/
@J8AnnDemos(value = { @J8AnnDemo(age = 0),@J8AnnDemo(age = 2) })
public class Test2 {
    public static void main(String[] args){
        Class<Test2> clazz = Test2.class;
        J8AnnDemo[] j8s = clazz.getDeclaredAnnotationsByType(J8AnnDemo.class);
        for (J8AnnDemo j8AnnDemo : j8s) {
            System.out.println(j8AnnDemo.name()+" = " + j8AnnDemo.age());
        }
        J8AnnDemos container = clazz.getDeclaredAnnotation(J8AnnDemos.class);
        System.out.println(container);
    }
    /*
     * raindrops = 0
raindrops = 2
@com.zuiliushang.annotationdiy.J8AnnDemos
(value=[@com.zuiliushang.annotationdiy.J8AnnDemo(
name=raindrops, age=0), @com.zuiliushang.annotationdiy
.J8AnnDemo(name=raindrops, age=2)])

     * */
}* */
}

Java8 新增的 Type Annotation

Java8 位 ElementType 枚举增加了 TYPE_PARAMETER、TYPE_USE 两个枚举值,这样就允许定义枚举时使用 @target(ElementType.TYPE_USE)修饰,这种注解被称为 Type Annotation(类型注解),Type Annotation 可用在任何用到类型的地方。

在 Java8 以前,只能在定义各种程序元素(定义类、定义接口、定义方法、定义成员变量……)时使用注解。从 Java8 开始,Type Annotation 可以在任何用到类型的地方使用。比如:

  • 创建对象(用 new 关键字创建)。
  • 类型转换。
  • 使用 implements 实现接口。
  • 使用 throws 声明抛出异常。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.