JAVA知识学习笔记QAQ
Anntation 注释/注解(基本 Annotation)
学过一次JAVA 但是因为一段时间没接触现在学习J2EE很吃力,决定回来填坑。
数组引用变量只是一个引用,这个引用变量可以指向任何有效的内存,只有当该引用指向有效内存后,才可通过该数组变量来访问数组元素。
与所有引用变量相同的是,引用变量是访问真实对象的根本方式。也就是说,如果希望在程序中访问数组对象本身,则只能通过这个数组的引用变量来访问它。
实际的数组对象被存储在堆(heap)内存中;如果引用该数组对象的数组引用变量是一个局部变量,那么它被存储在栈(stack)内存中。
当一个方法被调用时,方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁。因此,所有在方法中定义的局部变量都是放在栈内存中的;在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存结束后,这个对象还可能被另一个引用变量所引用(在方法的参数传递时很常见),则这个对象依然不会被销毁。只有当一个对象没有任何引用变量引用它时,系统的垃圾回收器才会在合适的时候回收它。
如果堆内存中数组不再有任何引用变量指向自己,则这个数据将成为垃圾,该数组所占的内存将会被系统的垃圾回收机制回收。因此,为了让垃圾回收机制回收一个数组所占的内存空间,可以将该数组变量赋为 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 引用所指向的数组本身:
当执行b = a 语句时,系统将会把 a 的值赋给 b ,a 和 b 都是引用类型变量,存储的是地址。阴齿把 a 的值赋给 b 后,就是让 b 指向 a 所指向的地址。
此时: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;
时 仅仅只是在栈内存中定义了一个空引用,这个引用并没有任何有效的内存,当然无法执行数组的长度:
当执行了第二行代码arr = new int[5];
动态初始化后,系统将负责为该数组分配内存空间,并分配默认的初始值,所有数组元素都被赋值为 0 :
此时 arr
数组的每个元素的值都是 0 ,当循环为该数组的每个元素依次赋值后,此时每个数组元素的值都变成程序显式指定的值:
引用类型数组的数组元素是引用,因此情况变得更加复杂。每个数组元素里面存储的还是引用,它指向另一块内存,这块内存里存储了有效数据。
我们来看下这个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;
代码时,仅仅在栈内存中定义了一个引用变量,也就是一个指针,这个指针并未指向任何有效的内存区:
而当执行person = new Person[2];
时,程序对 person 数组执行动态初始化,动态初始化由系统为数组元素分配默认的初始值:null,即每个数组元素的值都是 null:
可以看出 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 实例。
此时 数组 person 的两个数组元素依然是 null , 直到程序运行 person[0] = zhangsan; person[1] = lisi;
时,程序依次将 zhangsan 赋给 person 数组的第一个元素,把lisi 赋给 person 数组的第二个元素, person 数组的两个元素将会指向有效的内存区:
多维数组其实也是一维数组,只不过只不过数组里面的引用元素指向的内存区的数据是一维数组。
看下面的一个Demo:
所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型仓鼠,也可称为类型实参)。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 类时,为 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();
}
}
首先是使用匿名内部类:
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 表达式的代码块只有一条语句,则可以省略花括号。
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 表达式的类型,也被称为 “目标类型(target type)”,Lambda 表达式的目标类型必须是“函数式接口(functional interface)”。函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。
当程序创建对象、数组等引用类型实体时,系统都会在堆内存中为之分配一块内存区,对象就保存在这块内存区中,当这块内存不再被任何引用变量引用时,这块内存就变成垃圾,等待垃圾回收机制进行回收。垃圾回收机制特征:
当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种:
当一个对象失去引用后,系统何时调用它的 finalize() 方法对它进行资源清理,何时它会变成不可达状态,系统何时回收它所占有的内存,对于程序完全透明。程序只能控制一个对象何时不再被任何引用变量引用,决不能控制它何时被回收。
程序无法精确控制Java 垃圾回收的时机 ,但可以强制系统进行垃圾回收——**这种强制只是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定。**大部分时候,程序强制系统垃圾回收后总会有一些效果。强制系统垃圾回收的方法:
protected void finalize() throws Throwable
当finalize() 方法返回后,对象消失,垃圾回收机制开始执行。
任何 Java 类都可以重写 Object 类的 finalize() 方法,在该方法中清理该对象占用的资源。如果程序终止之前始终没有进行垃圾回收,则不会调用失去引用对象的 finalize() 方法来清理资源。垃圾回收机制何时调用对象的 finalize() 方法时完全透明的,只有当程序认为需要更多的额外内存时,垃圾回收机制才会进行垃圾回收。因此,完全有可能出现一种情况:某个失去引用的对象只占用了少量内存,而且系统没有产生严重的内存需求,因此垃圾回收机制并没有试图回收该对象所占用的资源,所以该对象的 finalize() 方法也不会得到调用。
finalize() 方法具有如下 4 个特点:
由于 finalize() 方法并不一定会被执行,因此如果想清理某个类里打开的资源,则不要放在 finalize() 方法中进行清理。
对大部分对象而言,程序里会有一个引用变量引用该对象,这是最常见的引用方式。除此之外,java.lang.ref 包下提供了 3 个类: SoftReference、phantomReference 和 WeakReference,它们分别代表了系统对对象的 3 种引用方式:软引用、虚引用和弱引用。
这是 Java 程序中最常见的引用方式。程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象。当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收。
软引用需要通过 SoftReference 类来实现,当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用对象而言,当系统内存空间足够时,它不会被系统回收,程序也可以使用该对象;当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中。
弱引用通过 WeakReference 类实现,弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存,当然,并不是说当一个对象只有弱引用时,它就立即被回收——正如那些失去引用的对象一样,必须等待到系统垃圾回收机制运行时才会被回收。
虚引用通过 PhantomReference 类实现,虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那么它也没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和**引用队列(ReferenceQueue)**联合使用。
三种引用类都包含了一个 get() 方法,用于获取被他们所引用的对象。
数组引用变量只是一个引用,这个引用变量可以指向任何有效的内存,只有当该引用指向有效内存后,才可通过该数组变量来访问数组元素。
与所有引用变量相同的是,引用变量是访问真实对象的根本方式。也就是说,如果希望在程序中访问数组对象本身,则只能通过这个数组的引用变量来访问它。
实际的数组对象被存储在堆(heap)内存中;如果引用该数组对象的数组引用变量是一个局部变量,那么它被存储在栈(stack)内存中。
栈内存和堆内存:
当一个方法被调用时,方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁。因此,所有在方法中定义的局部变量都是放在栈内存中的;在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存结束后,这个对象还可能被另一个引用变量所引用(在方法的参数传递时很常见),则这个对象依然不会被销毁。只有当一个对象没有任何引用变量引用它时,系统的垃圾回收器才会在合适的时候回收它。
如果堆内存中数组不再有任何引用变量指向自己,则这个数据将成为垃圾,该数组所占的内存将会被系统的垃圾回收机制回收。因此,为了让垃圾回收机制回收一个数组所占的内存空间,可以将该数组变量赋为 null ,也就切断了数组引用变量和实际数组之间的引用关系,实际的数组也就去成了垃圾。
只要类型相互兼容,就可以让一个数组变量指向另一个实际的数组,这种操作会让人产生数组的长度可变的错觉。
计算机高级语言按程序的执行方式可以分为编译型和解释型两种。
编译型语言是指使用专门的编译器,针对特定平台(如操作系统)将某种高级语言源代码一次性“翻
译”成可悲该平台硬件执行的机器码(包括机器指令和操作数),并包装成该平台所能识别的可执行性程
序的格式,这个转换过程称之为编译(Compile)。编译生成的可执行程序可以脱离开发环境,在特定
的平台上独立运行。
有些程序编译结束后,还可能需要对其他编译好的目标代码进行链接,即组装两个以上的目标代码模
块生成最终的可执行程序,通过这种方式实现低层次的代码复用。
因为编译型语言是一次性地编译成机器码,所以可以脱离开发环境独立运行,而且通常运行效率较高;但因为编译型语言的程序被编译成特定平台上的机器码,因此编译生成的可执行行程序通常无法移植到其他平台上运行;如果需要移植,则必须将源代码复制到特定平台上,针对特定平台进行修
改,至少也需要采用特定平台上的编译器重新编译。
现有的C、C++、Objective-C、Pascal 等高级语言都属于编译型语言。
解释型语言是指使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行的语言。解释型
语言通常不会进行整体性的编译和链接处理,解释型语言相对于把编译型语言中的编译和解释过程混合
在一起同时完成。
可以认为:每次执行解释型语言的程序都需要进行一次编译,因此解释型语言的程序运行效率通常较
低,而且不能脱离解释器独立运行。但解释器负责将源程序解释成特定平台的机器指令即可。解释型语
言可以方便地实现源程序级的移植,但这是以牺牲程序执行效率为代价的。
现有的 Ruby、Python 等语言都属于解释型语言。
除此之外,还有一种伪编译型语言,如 Visual Basic ,它属于半编译型语言,并不是真正的编译型语
言。它首先被编译成P-代码,并将解释引擎封装在可执行性程序内,当运行程序时,P-代码会被解析成
真正的二进制代码。表面上看起来 , Visual Basic 可以编译生成可执行的 EXE 文件,而且这个 EXE 文
件也可以脱离开发环境,在特定平台上运行,非常像编译型语言。实际上,在这个 EXE 文件中,既有
程序的启动代码,也有链接解释程序的代码,而这部分代码负责启动 Vsiual Basic 解释程序,再对
Visual Basic 代码进行解释并执行。
Java 语言比较特殊,由 Java 语言编写的程序需要经过编译步骤,但这个编译步骤并不会生成特定平
台的机器码,而是生成一种与平台无关的字节码(也就是*.class 文件)。当然,这种字节码不是可执行
性的,必须使用 Java 解释器来解释执行。因此可以认为: Java 语言既是编译型语言,也是解释型语
言。或者换个角度:Java 既不是纯粹的解释器语言,也不是纯粹的编译型语言。Java 程序的执行过程
必须经过先编译、后解释两个步骤:
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 对象。
构造器重载的方法和方法重载基本相似:只要求构造器的名字相同;为了让系统区分不同的构造器,多个构造器的参数列表必须不同。
MessageFormat 是抽象类 Format 的子类,Format 抽象类还有两个子类:NumberFormat 和 DateFormat,它们分别用于实现数值、日期的格式化。NumberFormat、DateFormat 可以将数值、日期转换成字符串,也可以将字符串转换成数值、日期。
NumberFormat 也是一个抽象基类,所以无法通过它的构造器来创建 NumberFormat 对象,它提供了如下几个类方法来得到 NumberFormat 对象:
示例代码:
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 也是一个抽象类,它提供了如下方法:
每种方法后面可以传入多个参数,用于指定样式和 Locale等参数;如果不指定则使用默认样式。
实例:
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 的子类。
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 修饰的成员变量而言,一旦有了初始值,就不能被重新赋值,如果既没有在定义成员变量时制定初始值,也没有在初始化块、构造器中为成员变量指定初始值,那么这些成员变量的值将一直是系统默认分配的0、'\u0000'、 false 或 null , 这些成员变量也就完全失去了存在的意义。因此 Java 语言规定: 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 变量本质就是一个 “宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。
通过 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 类代表 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());
}
}
例子:
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 以前,每个 catch 块只能捕获一种类型的异常;但从 Java 7 开始,一个 catch 块可以捕获多种类型的异常。
使用一个 catch 块捕获多种类型的异常时需要注意如下两个地方。
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 块后的异常参数,程序即可通过该参数来获得异常的相关信息。
所有的异常对象都包含了如下几个常用方法:
Java 的异常被分为两大类: Checked 异常和 Runtime 异常(运行时异常)。所有的 RuntimeException 类及其子类的实例被称为 Runtime 异常;不是 RuntimeException 类及其子类的异常实例则被称为 Checked 异常。
对于 Checked 异常的处理方式有如下两种:
只有 Java 语言提供了 Checked 异常, Checked异常体现了 Java 的严谨性,它要求程序猿必须注意该异常 —— 要么显式声明抛出,要么显式捕获并处理它,总之不允许对 Checked 异常不闻不问。这是一种非常严谨的设计,可以增加程序的健壮性。但是大部分的方法总是不能明确地知道如何处理异常,因此只能声明抛出该异常,而这种情况非常普遍,所以 Checked 异常降低了程序开发的生产率和代码的执行效率。
使用 throws 声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果 main 方法也不知道如何处理这种类型的异常,也可以使用 throws 声明抛出异常,该异常将交给 JVM 处理。
JVM处理异常的方法是:打印异常的跟踪栈信息,并且中断程序运行!
当程序出现错误时,系统会自动抛出异常;除此之外, 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 | √ |
从 JDK 5 开始, Java 增加了对元数据 (MetaData) 的支持,也就是 Annotation(注释、注解),Annotation 其实是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行响应的处理。通过使用注解,程序可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署。
Annotation 提供了一种伪程序元素设置元数据的方法,从某些方面来看, Annotation 就像是修饰符一样,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被存储在 Annotation 的 “name=value” 对中。
Annotation 是一个接口,程序可以通过反射来获取指定程序元素的 Annotation 对象,然后通过 Annotation 对象来取得注解里的元数据。要注意的是,有的 Annotation 指的是 java.lang.Annotation 接口,有的指的是 注解本身。
Annotation 必须使用工具处理,工具负责提取 Annotation 里包含的元数据,工具还会根据这些元数据增加额外的功能。
Java提供的5个基本 Annotation 的用法 —— 使用 Annotation 时要在其前面增加 @ 符号,并把该 Annotation 当成一个修饰符来使用,用于修饰它支持的程序元素。
5个基本的 Annotation 如下:
@OverRide 就是用来指定方法覆盖的,它可以强制一个子类必须覆盖父类的方法:
public class Fruit {
public void info(){
System.out.println("我是水果");
}
}
class Apple extends Fruit{
@Override
public void info(){
System.out.println("我是苹果");
}
}
如果没有覆盖父类方法,那么将会报错,可以保证子类绝对会覆盖父类方法,可以防止覆盖方法写错的问题。
@OverRide 只能修饰方法,不能修饰其他程序元素。
@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 指示被该 Annotation 修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告。@SuppressWarnings会一直作用于该程序元素的所有子元素。
public class Vegetables {
@SuppressWarnings({ "rawtypes", "unused" })
public static void main(String[] args){
ArrayList vegetables = new ArrayList();
}
}
在泛型擦除时,可能会引发问题:
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 规定:如果接口中只有一个抽象方法(可以包含多个默认方法或多个 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 对象时隐式执行的,而且它们总会全部执行,因此完全可以把多个普通初始化块合并成一个初始化块没从而可以让程序更加简洁,可读性强。
从某种程序上来看,初始化块是构造器的补充,初始化块总是在构造器执行之前执行。系统同样可使用初始化块来进行对象的初始化操作。
与构造器不同的是,初始化块是一段固定执行的代码,它不能接受任何参数,因此初始化块对同一个类的所有对象所进行的初始化处理完全相同。基于这个原因,不难发现初始化块的基本用法,如果有一段初始化块处理代码对所有对象完全相同,且无须接受任何参数,就可以把这段初始化处理代码提取到初始化块中。
实际上,初始化块只是一个假象,使用 javac 命令编译 Java 类后,该 Java 类中的初始化块会消失 —— 初始化块中代码会被“还原“ 到每个构造器中,且位于构造器所有代码的前面。
如果定义初始化块时使用了 static 修饰符,则这个初始化块就变成了静态初始化块,也被称之为类初始化块(普通初始化块负责对对象执行初始化,类初始化块则负责对类进行初始化)。静态初始化块是泪相关的,系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行。因此静态初始化块总是比普通初始化块先执行。
静态初始化时类相关的,用于对整个类进行初始化处理,通常用于对类变量执行初始化处理。静态初始化块不能对实例变量进行初始化处理。
在 Java 类里只能包含成员变量、方法、构造器、初始化块、内部类(包括接口、枚举) 5 种成员,其中 static 修饰的成员就是累成员。类成员属于整个类,而不属于单个对象。
类变量属于整个类,当系统第一次准备使用该类时,系统会为该类变量分配内存空间,类变量开始生效,直到该类被卸载,该类的类变量所占用的内存才会被系统的垃圾回收机制回收。类变量生存范围几乎等同于该类的生存范围。当类初始化完成后,类变量也被初始化完成。
类变量既可通过类来访问,也可以通过类的对象来访问。但通过类的对象来访问类变量时,实际上并不是访问该对象所拥有的变量,因为当系统创建该类的对象时,系统不会再为类变量分配内存,也不会再次对类变量进行初始化,也就是说,对象根本不拥有对应类的类变量,通过对象访问类变量只是一种假象,通过对象访问的依然是该类的类变量,可以这样理解:当通过对象来访问类变量时,系统会在底层转换为该类来访问类变量。
很多语言都不允许通过对象访问类变量,对象只能访问实例变量;但类变量必须通过类来访问。
由于对象实际上并不持有类变量,类变量是由该类持有的,同一个类的所有对象访问类变量时,实际上访问的都是该类所持有的变量。因此,从程序运行表面来看,即可看到同一类的所有实例的类变量共享同一块内存区。
类方法也是类成员的一种,类方法也是属于类的,通常直接使用类作为调用者来调用类方法,但也可以使用对象来调用类方法。与类变量类似,即使使用对象来调用类方法,其效果也与采用类来调用类方法完全一样。
当使用实例来访问类变量时,实际上依然是委托给该类来访问类成员,因此即使某个实例为 null ,它也可以访问它所属类的类成员。
如果一个 null 对象访问实例成员(包括实例变量和实例方法),将会引发 NullPointerException 异常,因为 null 表明该实例根本不存在,既然实例不存在,那么它的实例变量和实例方法自然也不存在。
静态初始化块也是类成员的一种,静态初始化块用于执行类初始化动作,在类的初始化阶段,系统会调用该类的静态初始化块来对类进行初始化。一旦该类初始化结束后,静态初始化将不会获得执行的机会。
如果一个类始终只是创建一个实例,则这个类被称为单例类。
根据良好的封装的原则:一旦把该类的构造器隐藏起来,就需要提供一个 public 方法作为该类的访问点,用于创建该类的对象,且该方法必须使用 static 修饰(因为调用该方法之前还不存在对象,因此使用该方法的不可能是对象,只能是类)。
除此之外,该类还必须缓存已经创建的对象,否则该类无法知道是否曾经创建过对象,也就无法保证值创建一个对象。为此该类需要使用一个成员变量来保存曾经创建的对象,因为该成员变量需要被上面的静态方法访问,故该成员变量必须使用 static 修饰。
public class Singleton {
private static Singleton instance;
static{};
public static Singleton getInstance(){
if(instance==null){
instance = new Singleton();
}
return instance;
}
}
Collection 接口是 List、Set 和 Queue 接口的父接口,Collection接口里定义了如下操作集合元素的方法:
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 类型。但是,我们可以使用泛型来限制集合里元素的类型,并让集合记住所有集合元素的类型。
Iterator 接口隐藏了各种 Collection 实现类的底层细节,向应用程序提供了遍历 Collection 集合元素的同一编程接口。Iterator 接口里定义了如下方法:
示例代码:
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]
}
}
当程序调用 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));
}
}
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 运算符的前一个操作通常是一个引用类型变量,厚一个操作数通常是一个类(也可以是接口),用于判断前面的对象是否是后面的类,或者是其子类、实例类的实例。如果是,返回 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 程序的国际化主要通过如下三个类来完成:
为了实现国际化,必须先提供程序锁需要的资源文件。资源文件命名可以有一下形式:
其中 baseName 是资源文件的基本名,可以随意指定;而 language 和 country 不可随意变化,必须是Java 所支持的语言和国家。
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());
}
}
}
子类扩展父类时,子类可以从父类继承得到成员变量和方法,如果访问权限允许,子类可以直接访问父类的成员变量和方法,相当于子类可以直接服用父类的成员变量和方法,确实非常方便。
但继承带来了高度复用的同时,也带来了一个严重的问题:继承严重地破坏了父类的封装性。因为:
每个类都应该封装它内部信息和实现细节了,而值暴露必要的方法给其他类使用。但在继承关系中,子类可以直接访问父类的成员变量(内部信息)和方法,从而造成子类和父类的严重耦合。
这导致了:父类的实现细节对子类不再透明,子类可以访问父类的成员变量和方法,并可以改变父类方法的实现细节(如通过方法重写的方式来改变父类的方法实现),从而导致了子类可以而已篡改父类的方法。
所以,为了保证父类具有良好的封装性,不会被子类随意改变,设计父类通常应遵循如下规则:
而当我们需要从父类派生新的子类时,不仅要保证子类时一种特殊的父类,而且需要具备以下两个条件之一:
如果需要复用一个类,除了把这个类当成基类来继承之外,还可以把该类当成另一个类的组成成分,从而允许心累直接服用该类的 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 除了在 java.lang 下提供了 5个基本的 Annotation 之外,还在 java.lang.annotation 包下提供了 6 个 Meta Annotation(元 Annotation),其中有 5 个元 Annotation 都用于修饰其他的 Annotation 定义。其中 @repeatable 专门用于定义 Java 8 新增的重复注解,后面补充,这里先说明4个元 Annotation。
@retention 只能用于修饰 Annotation 定义,用于指定被修饰的 Annotaion 可以保留多长时间,@retention 包含一个 RetentionPolicy 类型的 value 成员变量, 所以使用 @retention 时必须为该 value 成员变量指定值。
value 成员变量的值只能是如下三个:
如果需要通过反射获取注解信息,就需要使用 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 也只能修饰一个 Annotation 定义,它用于指定被修饰的 Annotation 能用于修饰哪些程序单元。@target 元 Annotation 也包含一个名为 value 的成员变量,该成员变量的值只能是如下几个:
与使用 @retention 类似的是,使用 @target 也可以直接在括号里指定 value 值,而无须使用 name=value 的形式。
@documented 用于指定被该元 Annotation 修饰的 Annotation 类将被 javadoc 工具提取成文档,如果定义 Annotation 类时使用了 @documented 修饰,则所有使用该 Annotation 修饰的程序元素的 API 文档中将会包含该 Annotation 说明。
@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
}
}
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
GBK编码中一个中文字符占两个字节,一个英文字符占一个字节;
UTF-8编码中一个中文字符占三个字节,一个英文字符占一个字节;
JAV是双字节编码,是UTF-16be编码,一个中文字符占两个字节,一个英文字符占两个字节。
在中文系统上默认的是ansi编码,即GBK编码。
当字节序列是某种编码时,若把字节序列变成字符串,也需要用这种编码方式,否则会乱码。
例如:
Java 5 新增了一个 enum 关键字(它与 class、interface 关键字的地位相同),用以定义枚举类。枚举类是一个特殊的类,它一样可以有自己的成员变量、方法,可以实现一个或者多个接口,也可以定义自己的构造器。 一个 Java 源文件中最多只能定义一个 public 访问权限的枚举类,且该 Java 源文件也必须和该枚举类的类名相同。
枚举类和普通类的区别:
枚举类默认提供了一个 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);
}
}
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 语言里 , 类才是“一等公民”,离开了类和对象的方法不能独立存在。
因此,如果定义方法,则只能在类体内定义,不能独立定义一个方法。一旦将一个方法定义在某个类中,如果这个方法使用了 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 。
实际上,运行程序的时候,系统分别为 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:
接下来,main() 方法中开始调用 swap() 方法, main() 方法并未结束,系统会分别为 main() 和 swap() 开辟出两个栈区,用于存放 main() 和 swap() 方法的局部变量。调用 swap() 方法时, dw 变量作为实参传入 swap() 方法,同样采用值传递方式:把 main() 方法里 dw 变量的值赋给 swap() 方法里的 dw 形参,从而完成 swap() 方法的 dw 形参的初始化。值得指出的是,main() 方法中的 dw 是一个引用 (也就是一个指针),它保存了 DataWrap 对象的地址值,即也会引用到堆内存中的 DataWrap 对象:
可以看出,这种参数传递方式确实是值传递方式,系统一样赋值了 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 的改变而改变:
大部分的时候,类被定义成一个单独的程序单元。在某些情况下,也会把一个类放在另一个类的内部定义,这个定义在其他类内部的类就被称为内部类(有的地方叫嵌套类),包含内部类的类也被称为外部类(也叫宿主类)。
从语法来看,定义内部类与定义外部类的语法大致相同,内部类除了需要定义在其他类里面之外,还:
只要把类放在另一个类内部定义就是定义了一个内部类:
public class Out{
class In{}
}
如果使用 static 修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象,因此使用 static 修饰的内部类被称为类内部类,有的地方也称为静态内部类。
把一个内部类放在方法里面定义,则这个类就是一个局部内部类 = =
匿名内部类适合创建那种只需要一次使用的类。创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用。
匿名类定义格式:
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);
}
}
正则表达式是一个强大的字符串处理工具,可以对字符串进行查找、提取、分割、替换等操作。String 类里也提供了如下几个特殊的方法:
除了上面特殊的方法都依赖于 Java 提供的正则表达式之外, Java 还提供了 Pattern 和 Matcher 两个类专门用于提供正则表达式支持。
正则表达式所包含的合法字符:
字符 | 解释 |
---|---|
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 正则表达式,这看起来比较繁琐。实际上,正则表达式还提供了数量标识符,正则表达式支持的数量标识符有如下几种模式:
三种模式的数量表示符:
贪婪模式 | 勉强模式 | 占用模式 | 说明 |
---|---|---|---|
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 类型使用@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 信息。
Java 使用 Annotation 接口来代表成员元素前面的注解,该接口是所有注解的父接口。 Java 5 在 java.lang.reflect 包下新增了 AnnotatedElement 接口,该接口代表程序中可以接受注解的程序元素。该接口主要有如下几个实现类:
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 信息:
为了获得程序中的程序元素 (如 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:
在 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 位 ElementType 枚举增加了 TYPE_PARAMETER、TYPE_USE 两个枚举值,这样就允许定义枚举时使用 @target(ElementType.TYPE_USE)修饰,这种注解被称为 Type Annotation(类型注解),Type Annotation 可用在任何用到类型的地方。
在 Java8 以前,只能在定义各种程序元素(定义类、定义接口、定义方法、定义成员变量……)时使用注解。从 Java8 开始,Type Annotation 可以在任何用到类型的地方使用。比如:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.