目录
1.基本数据类型和包装类
Java是面向对象的语言,也包含了8种基本类型。8种基本类型也给我们带来了很大的好处,但是在某些时候,基本类型就有些限制了。我们知道一般的引用类型都是java.lang.Object的子类,但是8种基本类型不是,也不支持面向对象的机制。为解决这个问题Java提供了包装类(Wrapper Class)的概念,为这8个基本类型提供了相应的引用类,称为基本类型的包装类。
基本数据类型和对应包装类
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
char | Character |
float | Float |
double | Double |
boolean | Boolean |
把基本数据类型变量包装成包装类实例时通过对应包装类的构造器实现的。除了Character之外,其他的构造器可以把字符串转为对应类型。
使用字符串创建Byte、Short、Integer、Long、Float、Double等包装类对象时。日后传入的字符串不能成功转型为对应的基本类型变量,则会引发java.lang.NumberFormatException异常。如果使用一个字符创建Boolean对象时,如果传入的是“true”(不区分大小写),创建结果为true;如果传入的是其他的字符串,都会创建false对象。
1.1自动拆箱和自动装箱
Java提供的基本类型和包装对象之间转换有点繁琐,在JDK1.5之后提供了自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)功能。
自动装箱:把基本类型直接赋值给对应的包装类型变量。
自动拆箱:直接把包装类型赋值给基本类型。
除了Character之外的所有包装类都有一个parseXxx(String s)
静态方法,用于将一个特定的字符串类型转换成基本类型变量。除此之外,在String类里也提供了多个重载valueOf()方法,用于将基本类型变量转换成字符串。
2.处理对象
2.1 toString
toString() 方法相当于“自我描述”,当一个类没有重写时,直接输出的结果是类型+@+hashCode
例如:Person@72617
。
我们可以重写这个方法,并自定义要输出的内容。
2.2==与equals比较运算符
==:当比较的数据类型是基本类型时,只要值相等就相等;当比较类型为引用类型时,比较的是对象,同一个对象才是相等的。若一个引用对象对应的值与另一个引用的对象对应的值完全相等,但是这是两个不同的对象,==
的结果是false
equals:equals对于一般的对象与==
区别不大。重点在于你可以重写equals方法,shi得比较的东西不一样。
对于String
类型来说,重写了equals方法,==
与equals
区别:==
比较的是两个对象,而equals方法则是比较值。
注意:如果说equals比较的是值,这种说法是错误的。对于String类型是这样,是因为String类重写了equals。因此,我们可以重写equals方法,自定义比较方法。
-
重写equals方法应该满足以下条件:
-
- 自反性:对于任意的x,满足
x.equals(x);
一定是true。
- 自反性:对于任意的x,满足
-
- 对称性:对于任意的x,y,如果
x.equals(y);
,同样满足y.equals(x);
结果是相同的。
- 对称性:对于任意的x,y,如果
-
- 传递性:对于任意的x,y,z,如果满足
x.equals(y);
为true,且y.equals(z);
为true,则必定满足x.equals(z);
为true。
- 传递性:对于任意的x,y,z,如果满足
-
- 一致性:对于任意的x,y,不管
x.equals(y);
调用多少次结果始终是一样的。
- 一致性:对于任意的x,y,不管
-
- 对于任意一个非null对象x,
x.equals(null);
为false
- 对于任意一个非null对象x,
2.3单例(Singleton)类
如果一个类只能创建一个实例,则这个类被称为单例类。
在某些特殊情况下,要求不允许自由创建该类的对象。而是只允许该类创建一个对象。为了避免其他类自由创建该类的实例,我们把该类的构造器是指为private修饰,从而把该类的构造器隐藏起来。
根据良好封装的原则:一旦把该类的构造器隐藏起来,则需要提供一个public方法作为该类的对象,且该类方法必须使用static修饰(因为调用方法之前还不存在对象,因此调用该方法的不可能是对象,只能是类)。
除此之外,该类无法知道是否曾经创建过对象,也就无法保证只创建一个对象,为此该类需要使用一个属性来保存曾经创建的对象,因为该属性需要别上面的静态方法访问,故此该属性必须使用static修饰。
/**
* 一个单例类
* @author Archer
*
*/
public class Singleton {
//使用一个变量来缓存曾经创建的实例
private static Singleton instance;
//构造器使用private修饰,隐藏该构造器
private Singleton(){}
/**
* 提供一个静态方法。来返回该实例
* 该方法加入自定义控制,保证只产生一个Singleton对象
* @return
*/
public static Singleton getInstance(){
//如果instance为null,表明没有创建过
if(instance==null){
//创建一个对象,并缓存起来
instance=new Singleton();
}
//如果instance不为null,表明已经创建过
return instance;
}
}
public class TestSingleton {
public static void main(String[] args){
//创建Singleton的对象不能通过构造器,只能通过getInstance方法
Singleton s1=Singleton.getInstance();d
Singleton s2=Singleton.getInstance();
System.out.print(s1==s2);
}
}
2.4final类和不可变类
-
final类
- final修饰的不可有子类,例如java.lang.Math类就是一个final类。 不可变类
- 不可变(immutable)类的意思是创建该类的实例后,该类的实例属性时不可改变的。
- 自定义不可变类的规则:
-
- 使用private和final修饰符来修饰类的属性。
-
- 提供带参数的构造器,用于传入参数类初始化类的属性。
-
- 仅为该类的属性提供getter方法,不要为该类提供setter方法,因为普通方法无法修改final修饰的属性。
-
- 如果有必要,重写Object类中hashCode和equals方法。在equals方法根据关键属性来作为来个对象相等的标准,除此之外,还应该保证两个用equals方法来判断为相等的对象的hashCode也相等。
- 与不可变类对应的是可变类,可变类就是属性可变的,大部分情况我们创建的都是可变类,特别是JavaBean,我们总是为其属性提供setter和getter方法。与可变类相比,不可变量类对象在整个生命周期中永远处于初始化状态,它的属性不可改变。
- 不可变类实例的状态不可改变,可以很方便被多个对象共享。如果经常需要使用相同的不变实例,可以考虑使用这种不可变类。
public class CacheImmutale {
private final String name;
private static CacheImmutale[] cacheImmutales=new CacheImmutale[10];
private static int position =0;
private CacheImmutale(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static CacheImmutale vauleOf(String name){
//遍历以缓存对象
for (int i = 0; i< position; i++){
//如果已有相同的实例,直接返回该缓存的实例
if (cacheImmutales[i]!=null&&cacheImmutales[i].getName().equals(name)){
return cacheImmutales[i];
}
}
//如果缓存池已满
if (position ==10){
//把缓存的第一个对象覆盖
cacheImmutales[0]=new CacheImmutale(name);
//将position设置为1
position=1;
return cacheImmutales[0];
}else {
//把新创建的对象缓存起来,position加1
cacheImmutales[position++]=new CacheImmutale(name);
return cacheImmutales[position-1];
}
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CacheImmutale){
CacheImmutale c1= (CacheImmutale) obj;
if (name.equals(c1.getName())){
return true;
}
}
return false;
}
@Override
public int hashCode(){
return name.hashCode();
}
public static void main(String[] args) {
CacheImmutale c1=CacheImmutale.vauleOf("hello");
CacheImmutale c2=CacheImmutale.vauleOf("hello");
//输出true
System.out.println(c1==c2);
}
}
上面的CacheImmutale类使用了一个数组来缓存该类的对象,这个对象长度为10,即该类共可以缓存10个CacheImmutale对象。当缓存池已满时,采用“先进后出”规则来决定那个对象将被移出缓存池。
如果直接使用new调用构造器来创建对象,对象一个会新增一个,无法控制,因此,我们可以将构造器属性设置为private。
是否使用缓存视情况而定,如果一个对象只使用一次,使用缓存弊大于利;反之,如果一个对象经常使用,使用缓存实例就是利大于弊。
2.5抽象类
当我们编写一个一个类时,会为该方法提供一个方法体。在某些情况下,父类知道子类会有这个方法,却不知道子类是怎么实现的,这时候可以使用抽象方法,抽象方法没有方法体,也不能有方法体。抽象方法和抽象类都是使用abstract
修饰。抽象方法所在的类一定是抽象类,抽象类里面不一定会有抽象方法。
-
抽象方法抽象类的规则:
-
- 抽象类必须使用
abstract
修饰,抽象方法也必须使用abstract
修饰,抽象方法不能有方法体。
- 抽象类必须使用
-
- 抽象类不能被实例化。即无法使用new调用器构造器来创建实例,即使这个类里面没有抽象方法,也不能创建。
-
- 抽象类可以包含属性、方法(包括普通方法和抽象方法)、构造器、初始化块、内部类和枚举类。抽象类的构造器不是用于创建实例的,而是被器子类调用。
-
- 含有抽象方法的类(包括直接定义了一个抽象方法;继承一个抽象父类,但没有完全实现父类包含的抽象方法;以及实现一个接口,但没有完全实现一个接口所包含的抽象方法三种情况),只能被定义为抽象类。
注意:
- 抽象方法和空方法体不是一个概念
public abstract void test();
是一个抽象方法,它没有方法体。而public void test(){}
是个普通方法,它定义了方法体,只是什么都没有做。- 抽象类不能创建实例,只能被继承。
- abstract不能修饰属性、局部变量、构造器。
- final修饰的类不能被继承,abstract修饰的方法不能被重写,因此,abstract和final不能同时使用。
2.6更彻底的抽象:接口
抽象类从多个类中抽象出来的模板。而接口是一种更彻底的抽象,接口里不能包含普通的方法,接口中所有方法都是抽象的。接口定义的是一种规范,不提供任何实现。
接口的定义使用interface
关键字。
修饰符 interface 接口名 extends 父接口1,父接口2...{
零到多个常量定义...
零到多个方法定义...
}
-
语法说明:
-
- 修饰符只能是public或者省略,如果省略public访问控制符,默认采用包权限控制符。
-
- 接口名应与类名相同的命名规则。
-
- 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
-
- 由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口可以包含属性(只能是常量)、方法(只是抽象方法)、内部类(包括内部接口)和枚举类。
-
- 接口中的所有成员只能使用public修饰,如果不使用修饰符,默认就是public。对于属性,接口里只能是常量,因此系统会自动为这些属性增加static和final修饰符。也就是说,不管属性是否使用
public static final
修饰默认都是这三个,可以通过接口访问这些属性。而方法总是public abstract
来修饰。接口里的内部类和枚举类默认都是public static
来修饰。一个接口里面只能有一个public修饰的接口。
接口的继承
- 接口中的所有成员只能使用public修饰,如果不使用修饰符,默认就是public。对于属性,接口里只能是常量,因此系统会自动为这些属性增加static和final修饰符。也就是说,不管属性是否使用
- 接口的继承和类的继承不一样,接口完全支持多继承,一个接口可以有多个直接父接口。和类继承相似,子接口扩展父接口时,获得父接口所有定义的抽象方法、常量属性、内部类、枚举类。 使用接口
- 接口不能创建实例,但是可以直接用于声明引用类型的变量。当这个接口用于声明引用类型的变量时,这个引用类型的变量必须引用到实现类的对象。除此之外,接口的主要用途就是被实现类实现。
-
一个类可以实现一个或多个接口,使用
implements
关键字,多个接口之间用逗号(,)隔开。一个类实现一个接口之后,这个类必须完全实现这个接口内的所有方法。接口不能显式地继承任何类。
接口与抽象类
-
相同点
-
- 都不能创建实例,都位于继承数的顶端,用于被其他类继承。
-
- 都可以包含抽象方法,实现接口或者继承抽象类的普通子类都需要实现这些抽象方法。
不同点
-
- 接口中只能包含抽象方法,抽象类中可以有抽象方法,也可以没有抽象方法。
-
- 接口中的属性都是静态常量属性,抽象类中可以有静态常量属性,也可以有普通属性。
-
- 接口中不包含构造器,抽象类中包含构造器。
-
- 接口中不能包含初始化块,抽象类中完全可以包含初始化块。
-
- 一个类最多只能由一个直接父类,包括抽象类;但是一个类可以直接实现多个接口,接口弥补了Java单继承的不足。
2.7内部类
我们把一个类定义在另一个类内部,这样的类称为内部类(嵌套类),包含内部类的类称为外部类。(宿主类)。
注意:一个文件中有多个类,这些类不是定义在一个类内部的,这些类也不属于内部类。
-
内部类作用:
-
- 内部类提供了更好的封装性,可以把内部类隐藏在外部类中,避免同一个包下的其他类访问。
-
- 内部类成员可以直接访问访问外部类的私有属性,但是外部类不能访问内部类的实现细节,例如属性。
-
- 匿名内部类适合用于创建那些仅使用一次的类。
2.7.1非静态内部类
把一个类在另一个类中定义,这就是非静态内部类的定义(不使用static),当然也可以定义在方法内部,定义在方法内部的类也被称为局部内部类。
成员内部类是一种与属性、方法等相似的类成员;局部内部类和匿名类不是类成员。
当内部类的方法要访问某个变量时,首先从该方法中找有没有与该变量同名的变量;如果没有找到,再到该内部类中找;如果还是没有找到就会在该内部类对象的外部类中找;如果还是找不到就会出现编译错误。
如果内部类与外部类的属性同名,在内部类中,访问内部类的属性使用this
,访问外部的类属性外部类类名.this
为什么非静态内部类可以访问外部类的私有属性,而外部类不能访问内部类的属性呢?
非静态内部类必须寄存在外部类对象里,而有外部类对象时不一定有非静态内部类。也就是说,如果存在一个非静态内部类对象,则必定存在一个被他寄存的内存内部类;反之,若存在一个外部类,不一定会有一个内部类寄存在其中。
注意:非静内部态里面不能有静态方法、静态属性、静态初始化块。可以包含非静态初始化块。
2.7.2 静态内部类
用static修饰的内部类,称为静态内部类。这个内部类属于外部类,不是属于外部类的某个对象。因此,静态内部类也称类内部类。
静态内部类可以包含静态成员,也可以包含非静态成员。静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使静态内部类的实例方法也不能访问外部类的实例成员。
为什么静态内部类实例方法也不能访问外部类的实例属性呢?
因为静态内部类是与外部类相关的,是属于外部类的,而不是属于外部类的实例。也就是说静态内部类是寄存在外部类里的,而不是外部类的对象,当一个静态内部类存在时,并不一定存在一个被他寄存的外部类对象,静态内部类的对象里只有外部类的类引用,没有外部类对象的引用。如果允许静态内部类的实例方法访问外部类的实例成员时,但找不到被继承的外部类对象,这将会引起错误。
外部类依然不能直接访问静态内部类的成员,但是可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类的对象作为调用者来调用静态内部类的实例成员。
Java还允许接口里定义内部类,接口里定义内部类默认使用public static修饰,也就是说,接口的内部类只能是静态内部类。
接口能否定义内部接口?
可以。接口的内部接口是接口的成员,因此系统会给内部接口默认添加public static来修饰。如官方定义接口内部接口时,需要指定访问权限,也只能是使用public修饰。当然,定义内部接口的意义不大,因为接口的作用是定义一个公共规范,暴露出来供大家使用,如果接口定义为一个内部接口,意义何在?
2.7.3使用内部类
-
在外部内里使用内部类
-
在外部类中使用内部类,和使用普通类区别不大。可以通过new调用内部类的构造器来创建实例。就是不要在外部类的静态成员(包括静态初始化块和静态方法)中使用非静态内部类。
在外部类以外使用内部类
-
如果希望在静态内部类以外使用内部类(包括静态内部类和非静态内部类),则内部类不能使用private修饰。
-
静态内部类的访问权限:
-
- private: 只能在外部类中使用内部类
-
- 默认:也就是不使用修饰符,只能被与外部类同处于一个包内的其他内所访问。
-
- protected:可被与外部类同出一个包中其他类和外部类的子类访问。
-
- public:在任何地方都可以被访问。
-
在外部类以外的地方创建或调用非静态外部类要通过外部类的对象。创建非静态内部类的语法:
外部类实例.new 外部类类名();
非静态内部类
class Out(){
//定义一个非静态内部类,不使用修饰符,同包下课访问
class In{
public In(String msg){
System.out.println(msg);
}
}
}
定义一个类继承Out的内部类In
public clss SubClass extends Out.In{
//显示定义SubClass的构造器
public SubClass(Out out){
//通过传入的Out对象显式调用In的构造器
out.super("hello");
}
}
在上面代码中SubClas继承了Out类的内部类In,调用父类的构造器(Out的In这个内部类)时,通过Out的对象调用。super代表的是In这个类的构造器。
我们可以看出继承一个内部类作为父类时,调用这个内部类的构造器需要通过外部类。
非静态内部类的子类不一定是内部类,它可以是一个顶层类,但是非静态内部类的子类的实例一样需要保留一个引用,该引用指向器父类所在的外部类的对象。也就是说,如果有一个内部类子类的对象存在,则一定存在与之对应的外部类的对象。
在外部类以外使用静态内部类,无需创建一个内部类对象,因为这个内部类是属于这个类的,而不是这个类的对象。
上面的Out编译后会生成Out.class和Out$In.class两个文件,也就是说,内部类编译后生成的字节码文件名是外部类名$内部类名.class的格式。
既然内部类时外部类的成员,是否可以为外部类定义一个子类,然后在子类中定义一个内部类来重写父类的内部类?
: 不可以。内部类的类名不是简单地有内部类的类名组成,它实际上把外部类的类名作为一个命名空间,作为外部类类名的限制。因此,就是子类和父类的内部类同名,子类也不能重写这个内部类,在编译之后,这个两个类不是全同名的。
2.7.4局部内部类
如果把一个类定义在方法里,这个类就是局部内部类,这个局部内部类只在该方法内有效。局部内部类不能在外部类以外的地方使用,因此局部内部类无需使用房屋控制符和static修饰。
对于局部成员来说,不过是局部变量,还是局部内部类,他们的上一级都是方法。它们是属于方法的,而不是属于类,因此使用static修饰完全没有意义。不仅如此,局部成员的作用域也就是在方法内,其他程序单元不可能访问另一个方法中局部成员,所以所有局部成员使用访问控制符。
如果要使用局部内部类,创建实例或者派生出起来类也只能在方法内部执行。
public class LocalInnerClass{
public static void main(String[] args){
//定义局部内部类
class InnerClass{
int a;
}
//局部内部类的子类
class SubInnerClass extends InnerClass{
int b;
}
}
}
2.7.5匿名内部类
匿名内部类顾名思义这个内部类时没有名字的内部类。匿名内部类适合创建那种只需要使用一次的类。创建匿名内部类时会立即创建一个该类的实例,匿名内部类也只能使用一次。为什么匿名内部类只能使用一次呢?类名作为一个类的标志,而匿名内部类没有名字,这样我们怎么告诉系统我们要用的是哪个类呢?因此匿名内部类只能使用一次,不能重复使用。
匿名内部类语法:(定义匿名内部类不用class关键字)
new 父类构造器(实参列表)|实现接口(){
//匿名内部类的类体部分
}
从语法定义我们可以看到,匿名内部类必须继承一个父类,或者实现一个接口,但最多只能有一个父类,或者一个接口。
-
匿名内部类规则:
-
- 匿名内部类不能是抽象类。因为创建一个匿名内部类时,会立即创建一个该类的对象,而抽象类不能创建实例。
-
- 匿名内部类没有构造器,因为匿名内部类没有类名,所以也就不能有构造器。
由于匿名内部类不能是抽象的,继承抽象类时,需要重写里面的所有抽象方法,可以选择性地重写普通方法。继承接口时,一定要重写里面的所有方法(接口里面的所有方法默认都是抽象的)。
interface Product{
public String getName();
public double getPrice();
}
普通类
class HighEndProduct implements Product {
@Override
public String getName() {
return "cake";
}
@Override
public double getPrice() {
return 10;
}
}
匿名内部类
public class TestClass {
public void test(Product product){
System.out.println(product.getName()+" price:"+product.getPrice());
}
public static void main(String[] args) {
TestClass t=new TestClass();
t.test(new Product(){
@Override
public String getName() {
return "cake";
}
@Override
public double getPrice() {
return 10;
}
});
}
}
2.7.6闭包(Closure)和回调
闭包是一种能够被调用的对象,它保存了创建它的作用域的信息。
Java没有显式地支持闭包。但是对于非静态内部类来说,在创建它的时候,就记录了外部类的详细信息,还保留了一个创建非静态对象的引用,,并且可以直接调用外部类的private成员,因此,可以把非静态内部类当成面向对象的领域的闭包。
通过这种仿闭包的非静态内部类,可以很方便实现回调功能。回调就是某个方法一旦获得了内部类对象的引用后,就可以在合适的时机反过来调用外部类的实例方法和属性。回调就是允许客户类通过内部类引用来调用其外部类的方法。
public interface Teacher {
String name="teacher";
void work();
}
public interface Programmer {
String name="programmer";
void work();
}
假设现在又一个人即使老师,又是程序员。也就是说需要定义一个类即继承Teacher接口也要继承Programmer接口。看起来并没有什么问题,但是实际上Teacher和Programmer类里面都有work方法,这时候我们该怎么办呢。
public class TeacherProgrammer implements Teacher, Programmer{
@Override
public void work() {
//Reference to 'name' is ambiguous,
//both Teacher.name' and Programmer.name' match.
// System.out.println(name);
}
}
上面这段代码虽然继承了work方法,但是不知道继承的到底是哪个方法,我们看看IDEA中的警告。
当我们使用name
时会无法通过编译,IDE给我们的报错信息如下:
Reference to 'name' is ambiguous, both Teacher.name' and Programmer.name' match.
翻译过来就是对name的引用不明确 Teacher 中的变量 name 和 Programmer 中的变量 name 都匹配
,这时候系统都糊涂了,当然不能通过编译。
怎么解决这个问题。这时候我们就可以使用仿闭包的内部类实现这个功能了。
public class TeacherProgrammer implements Teacher{
@Override
public void work() {
System.out.println(name);
}
class ProgrammerInner implements Programmer{
@Override
public void work() {
System.out.println(name);
}
}
public Programmer getCallBackReference(){
return new ProgrammerInner();
}
}
2.8枚举类
2.8.1自定义枚举类和枚举类
在某些情况下,一个类的对象是有限且固定的,例如季节,只有四个固定的季节。这种实例有限且固定的类,在Java中称为枚举类。
当然我们也可以手动实现枚举类:
- 通过private来将构造器隐藏起来。
- 吧这个类的所有有可能实例都使用public static final属性来保存。
- 如果有必要,也可以提供一些静态方法,只允许其他程序根据特定参数获取与之匹配的实例
自定义枚举类 (以季节为例)
public class Season {
private final String name;
private final String desc;
public static final Season SPRING=new Season("春天","春暖花开");
public static final Season SUMMER=new Season("夏天","夏日炎炎");
public static final Season FALL=new Season("秋天","春秋高气爽");
public static final Season WINTER=new Season("冬天","雪花纷飞");
public Season(String name, String desc) {
this.name = name;
this.desc = desc;
}
public String getName() {
return name;
}
public String getDesc() {
return desc;
}
}
JDK1.5之后增加了枚举类。使用关键字enum关键字可以定义枚举类。枚举类是一种特殊的类,它一样可以有自己的方法和属性,可以实现一个或多个接口,也可以定义自己的构造器。一个Java源文件中最多只能定义一个public访问权限的枚举类,且Java的源文件必须和这个枚举类的类名相同。
-
枚举类与普通类的区别
-
- 枚举类可以实现一个或多个接口,使用enum默认是继承了java.lang.enum类而不是java.lang.Object类。其中java.lang.enum类实现了java.lang.Serilalizable和java.lang.Comparable这两个接口。
-
- 枚举类的构造器只能使用private访问控制符,如果省略构造器的控制符,默认就是private修饰。如果要强制性指定控制符,也只能是private。
-
- 枚举类的所有实例必须在枚举类中显示列出。否则这个枚举类将永远不能产生实例。列出的枚举类实例,默认就是使用
public static final
修饰,不需要显示添加。
- 枚举类的所有实例必须在枚举类中显示列出。否则这个枚举类将永远不能产生实例。列出的枚举类实例,默认就是使用
-
- 所有的枚举类都提供了一个values方法,这个可以很方便遍历所有枚举类。
public enum SeasonEnum {
SPRING,SUMMER,FALL,WINTER
}
上面这段代码编译之后,会产生一个SeasonEnum.class文件,这表明枚举类是一个特殊的Java类。
定义枚举类时,需要显式列出所有枚举值,如上面的SPRING,SUMMER,FALL,WINTER他们之间是有逗号隔开(,)
public static void main(String[] args) {
for (SeasonEnum s:SeasonEnum.values()){
System.out.println(s);
}
}
上面迭代输出了SeasonEnum这个枚举类的所有实例属性,是通过values
获取所有实例属性。
-
java.lang.enum的几个方法:
-
int compareTo(E o);
:该方法用于指定枚举类对象比较顺序,同一个枚举类实例,只能与相同类型的枚举类实例进行对比。如果该枚举对象位于指定枚举对象之后,则返回正整数;如果该枚举类位于指定枚举类之前,返回负数;否则,返回0;
-
String name();
:返回此枚举实例的名称,这个名字就是定义枚举类时列出的所有的枚举值之一。与此方法相比,大多数程序员应该优先考虑使用toString()
,toString()
返回的更为友好。
-
int ordinal();
:返回枚举类中的索引值(就是枚举值在枚举类中声明的位置,第一个索引为0)。
-
String toString
:返回枚举常量名称,大致与name方法相同,toString()更常用。
-
public static <T extends Enum<T>> valuesOf(Class<T>enumType,String name);
这个是一个静态方法,用于返回指定枚举类中指定名称的枚举值。名称必须与在该枚举类中声明枚举值所用的标识符完全匹配,不允许使用额外的空白字符。
2.8.2枚举类的属性、方法、构造器
枚举类也是一种类,是一种比较特殊的类,因此也可以有属性和方法。如下:
public enum SeasonEnum {
SPRING,SUMMER,FALL,WINTER;
private String name;
public void setName(String name){
switch (this){
case SPRING:
if (name.equals("春天")){
this.name=name;
}else {
System.out.println("参数错误");
}
break;
case SUMMER:
if (name.equals("夏天")){
this.name=name;
}else {
System.out.println("参数错误");
}
break;
case FALL:
if (name.equals("秋天")){
this.name=name;
}else {
System.out.println("参数错误");
}
break;
case WINTER:
if (name.equals("冬天")){
this.name=name;
}else {
System.out.println("参数错误");
}
break;
}
}
public String getName() {
return name;
}
}
枚举类也可以有构造器,但是构造器只能是private修饰,若没有显式指定无参构造器,且有显式指定带参构造器,在创建这个枚举类时必须使用。
2.8.3实现接口的枚举类
枚举类和普通的类一样,枚举类也可以实现一个或者多个接口,也需要实现该接口的所有方法。使用枚举类继承接口不同点,是可以根据不同的枚举值,提供不同的实现方式,从而让不同枚举类调用该方法时,有不同的行为。
public interface GenderDesc {
void info();
}
public enum Gender implements GenderDesc{
MALE("男"){
@Override
public void info() {
System.out.println("this is a man");
}
},
FEMALE("女"){
@Override
public void info() {
System.out.println("this is a woman");
}
};
private String name;
Gender(String name) {
this.name = name;
}
}
当我们创建两个枚举值MALE
和FEMALE
时,后面紧跟一对花括号,这对花括号里面都重写了info();
方法。这个花括号里面的部分实际上是一个匿名类内部的类体部分,所有这个部分的代码语法和前面计算的匿名类内部类大致相同,
注意:上面的枚举类的实例声明创建,必须放在类的花括号里面的第一行。
编译上面这段代码后,产生了Gender.class、Gender$1.class、Gender$2.class。这表明,世界上MALE和FEMALE实际上是Gender匿名子类的实例,而不是Gender类的实例。这样当我们调用MALE和FEMALE两个枚举值的方法时,就会看到两个方法表现不同。
-
包含抽象方法的枚举类
- 枚举类定义抽象方法时,无需显式使用abstract关键字将枚举类定义成抽象类,但因为枚举类需要显式创建枚举值,而不是作为父类,所有定义每个枚举值是必须为抽象方法提供实现,否则会出现编译错误。
2.9对象与垃圾回收
当创建对象、数组等引用类型实体时,系统会在堆内存中为之分配一块内存区,对象就是保存在这块内存区里的,当这块内存不再被其他任何引用变量引用时,这块内存就变成垃圾,等待垃圾回收机制进行回收。
-
垃圾回收机制特征:
-
- 垃圾回收机制值负责回收堆内存中的对象,不会对任何物理资源(例如数据库连接、网络IO等资源)。
-
- 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候进行。当对象永久性地失去了引用后,系统会在合适的时候回收它所占的内存。
-
- 垃圾回收机制回收任何对象之前,总会调用它的
finalize
方法,该方法可能使该对象重新复活(让一个引用变量重新引用该变量),从而导致垃圾回收机制取消。
- 垃圾回收机制回收任何对象之前,总会调用它的
2.9.1对象在内存中的状态
当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把他所处状态分为3种.
-
对象在堆内存中三种所处状态:
-
- 激活状态:当一个对象被创建后,有一个以上的引用变量引用他,则这个对象在程序中处于激活状态,程序可以通过引用变量来调用该对象的属性和方法。
-
- 去活状态:如果程序中,某个对象不再有任何引用变量来啊引用它,它就进入了去活状态。在这个状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用finalize方法进行资源清理,如果系统的finalize方法重新让一个引用变量指向这个对象,则这个对象就会再次变为激活状态;否则这个对象将会进入死亡状态。
-
- 死亡状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize方法依然没有将改对象变为激活状态,这个对象将会永久失去引用,最后变为死亡状态。
当一个对象处于死亡状态时,才会真正回收该对象所占有的资源。
- 死亡状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize方法依然没有将改对象变为激活状态,这个对象将会永久失去引用,最后变为死亡状态。
对象的状态转换
一个对象可以被一个方法局部变量引用,也可以被其他类的类属性引用,或者其他对象的实例属性引用。当某个对象被其他类的类属性引用时,只有这个类被销毁后,该对象才会进入去活状态;当某个对象被其他类的对象的实例属性引用时,只有该对象被销毁后,该对象才会进入去活状态。
2.9.2强制性垃圾回收
当一个对象失去引用后,系统何时调用finalize方法对资源进行清理还是个未知数。程序只能控制一个对象何时不再被引用,但是不能绝对控制它何时被回收,因为当一个对象不再被任何引用变量引用时,系统不一定会里面进行垃圾回收处理。
虽然外面不能精确控制Java垃圾回收的时机,但是我们依然可以强制系统进行垃圾回收处理,注意,这种强制执行通知系统进行垃圾回收,但系统会不会听你的进行垃圾回收还不确定。大部分时候系统强制性系统垃圾回收后总会有一些效果。这就好比,规定不能酒后驾车,这就像是对你的一个通知一个警告,但是还是不免有些人还会酒后驾车,但是大部分人更加重视这一点了。这就像是强制性系统进行垃圾回收。
-
强制性垃圾回收方法:
-
- 调用System类的gc()静态方法:
System.gc();
- 调用System类的gc()静态方法:
-
- 调用RunTime对象的gc()实例方法:
RunTime.getRunTime().gc();
- 调用RunTime对象的gc()实例方法:
强制性垃圾回收,这个强制仅仅是建议系统立即精选垃圾回收,系统完全有可能并不立即进行垃圾回收,但垃圾回收机制也不会对程序的建议完全置之不理,垃圾回收机制会在接到通知后,尽快进行垃圾回收。
2.9.3finalize方法
当垃圾回收机制回收某种对象所占内存之前,通常要求程序调用finalize方法清理资源,在没有明确指定资源清理的情况下,Java提供了默认机制来清理该对象的资源。
finalize方法原型:
protected void finalize() throws Throwable
当finalize方法返回之后,对象消失,垃圾回收机制开始执行。这个方法的抛出Throwable异常,表示它可以抛出任何类型的异常。
任何Java类都可以覆盖Object类的finalize方法,在该方法中清理对象占用的资源。如果程序终止之前,始终没有进行垃圾回收,则不会调用失去引用对象的finalize方法来清理资源。垃圾回收机制合适调用对象的finalize方法是不确定的。只有当程序认为需要更多额外内存时,而且系统有严重的内存需求,垃圾回收机制才会进行垃圾回收。因此,可能当某个时期引用对象只占用了少量内存,而系统也没有产生严重的内存需求,这时候垃圾回收机制没有试图回收该对象所占资源,所以该对象的finalize方法也不被调用。
-
finalize方法4个特点:
-
- 永远不要主动调用某个对象的finalize方法,该方法一个交给垃圾回收机制调用。
-
- finalize方法何时被调用,是否调用有不确定性。不要finalize方法当成一点会被执行的方法。
-
- 当JVM执行去活对象的finalize方法时,可能该对象或系统中其他对象重新被激活状态。
-
- 当JVM执行finalize方法时出现了异常,垃圾回收机制不会报告异常,程序继续执行。
由于finalize方法并不一定会被执行,如果想保住某个类里打开的资源被清理,不要放在finalize方法里清理,有其他专门的用于资源清理的方法。
public class TestFinalize {
private static TestFinalize tf=null;
public void info(){
System.out.println("测试资源清理finalize方法");
}
public static void main(String[] args) throws InterruptedException {
new TestFinalize();
//通知系统进行资源回收
System.gc();
//让程序暂停2秒
Thread.sleep(2000);
tf.info();
}
@Override
protected void finalize() throws Throwable {
//让tf引用到视图回收的去活对象,即去活对象重新被激活
tf=this;
}
}
运行结果:
上面这段代码,先是创建了一个TestFinalize对象,因为创建对象后,没有把这个对象赋给任何一个引用变量,所以该对象立即进去去活状态。进入去活状态后,使用System.gc();
方法通知系统进行垃圾回收,垃圾回收机制执行之前调用finalize方法,而这个方法将刚刚创建的对象赋值给tf
,使得这个对象重新被引用,这样这个对象从去活状态变为激活状态。Thread.sleep(2000);
使系统暂停2秒。再次调用tf
对象的info方法时,正常执行。
如果删除Thread.sleep(2000);
,再次执行程序结果如下:
可以看出当我们删除Thread.sleep(2000);
后,即使使用System.gc();
方法,系统并没有立即进行垃圾回收,否则将会调用去活对象的finalize方法,也就是让tf
引用堆里的TestFinalize对象,就不会发生空指针异常了。当我们让系统暂停两秒后。程序暂停了。垃圾回收机制也收到了通知,调用了finalize方法。
2.9.4对象的软、弱、虚引用
对于大部分对象而言,程序里会有一个引用变量引用该对象,这种引用方式是最常见的引用方式。除此之外,java.lang.ref包下提供的三个类:SoftReference(软引用)、PhantomReference(虚引用)、WeakReference(弱引用)。
-
Java对象的引用:
-
- 强引用(StrongReference):这是最常见的引用方式,程序创建一个对象,并把这个对象赋给一个引用变量。程序通过该引用变量来操作实际对象,对象和数组都是采用了强引用的方式。当一个对象被一个或者一个以上的引用变量所引用时,它处于激活状态,不可能被垃圾回收机制回收。
-
- 软引用(SoftReference):软引用需要通过SoftReference类来实现,当一个对象只具有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统会回收它。软引用通常用于对内存敏感的程序中。
-
- 弱引用(WeakReference):弱引用通过WeakReference类实现,弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。当然,并不是说当一个对象只有弱引用时,它就会立即被回收—正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。
-
- 虚引用(PhantomReference):虚引用通过PhantomReference类实现,虚引用完全类似于没有引用。虚引用堆对象本身没有太大的影响,对象甚至感受不到虚引用的存在。如果一个对象只有一个虚引用时,那它和没有引用的效果大致相同。虚引用注意用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列(ReferenceQueue)联合使用。
上面三个引用类都包含了一个get方法,用于获取被它们引用的对象。
引用队列由于java.lang.ref.ReferenceQueue类表示,它用于保存被回收后对象的引用。当把软引用、弱引用和引用对象联合使用时,系统在回收被引用的对象之后,就把回收对象对应的引用添加到关联的引用队列中。与软引用和弱引用不同的是,虚引用在对象被释放之前,将把它对应的虚引用添加到它的关联引用队列中,这使得可以在对象被回收值之前采取行动。
软引用和弱引用可以单独使用,但虚引用不能单独使用,单独使用虚引用没有太大意义。虚引用的主要作用就是跟踪对象被垃圾回收的状态,程序可以检查与虚引用关联的引用队列中是否已经包含了该虚引用,从而了解虚引用对象是否即将被回收。
修饰符的适用范围
- | 顶层类/接口 | 成员属性 | 方法 | 构造器 | 初始化块 | 成员内部类 | 局部成员 |
---|---|---|---|---|---|---|---|
public | √ | √ | √ | √ | √ | ||
protected | √ | √ | √ | √ | |||
包访问控制符(默认) | √ | √ | √ | √ | ○ | √ | ○ |
private | √ | √ | √ | √ | |||
abstract | √ | √ | √ | ||||
final | √ | √ | √ | √ | √ | ||
static | √ | √ | √ | √ | |||
strictfp | √ | √ | √ | ||||
synchronized | √ | ||||||
native | √ | ||||||
transient | √ | ||||||
volatile | √ |
strictfp关键字含义是FP-strict,也就是精确浮点的意思。在Java虚拟机进行浮点运算时,如果没有指定strictfp关键字时,Java的编译器和运行环境在堆浮点运算上不一定令人满意。一旦使用strictfp来修饰类、接口、或者方法时,那么所修饰范围内Java的编译器运行时环境会完全依照浮点规范IEEE-754来执行。因此如果像让浮点运算更加精确,就可以使用strictfp关键字来修饰类、接口和方法。
native关键字主要用于修饰一个方法,使用native修饰的方法类似于一个抽象方法。与抽象方法不同的是,native修饰方法通常采用C语言来实现。如果某个方法需要利用平台的相关性,或者访问系统硬件等,则可以把该方法使用native修饰,在吧该方法教给C实现。一旦程序中包含native方法,这个程序将失去跨平台的功能。
2.11使用JAR
JAR全称为Java Archive File,就是Java档案文件。通常JAR是一种压缩文件,与ZIP压缩文件兼容,通常称为JAR包。JAR文件与ZIP文件的区别就是JAR文件 默认包含一个名为MATA-INF/MANIFEST.MF的清单文件,这个清单文件是在生成JAR文件由系统自动创建的。
当开发一个应用程序之后,这个程序包含了很多类,如果需要把这个也要程序给别人使用,通常会将这些文件打包成一个JAR文件,则JVM可以自动在内存中解压这个JAR包,把这个JAR包当成一个路径,在这个路径中查找所需要的类或包层次对应的路径结构。
-
使用JAR文件好处:
-
- 安全。能够对JAR文件进行数字签名,只让能够识别数字签名的用户使用里面的东西。
-
- 加快下载速度。在网上使用Applet时,如果存在多个文件而不打包,为了能够把这个文件都下载到客户端,需要为每个文件单独建一个HTTP连接,这是非常耗时的工作。使用这些文件压缩成一个JAR包,则只要建立一次HTTP连接就足够下载所有文件了。
-
- 压缩。使文件变小,JAR的压缩机制和ZIP完全相同。
-
- 包封装。能够让JAR包里的文件依赖于统一版本的类文件。
-
- 可移植性。JAR包作为内嵌在Java平台内部处理的标准,能够在各种平台上直接使用。
-
jar命令:
- jar是随JDK自动安装的,在JDK安装目录下的bin目录中,Windows下文件名为jar.exe,在Linux下文件名为jar。它的运行需要用到JDK安装目录下lib目录中的tool.jar文件。但通常系统会自动加载tool.jar,无需我们显式设置。
-
创建可执行JAR包
-
当一个应用程序开发成功后,发布方式有:
-
- 使用平台相关的编译器将整个应用编译成平台相关的可执行文件。这种方式需要第三方编译器的支持,而且编译生成的文件丧失了跨平台特性,甚至可能有一定的性能下降。
-
- 为整个应用编辑一个批处理文件。以Windows操作系统为例,批处理文件只需要定义
java package.MainClass
命令。当用户点击上面的批处理文件时,系统将执行;批处理文件的Java命令,从而运行程序的主类。如果不想保留运行Java程序的命令窗口,也可以使用start javaw package.MainClass
命令。
- 为整个应用编辑一个批处理文件。以Windows操作系统为例,批处理文件只需要定义
-
- 将一个程序制作成一个可执行的JAR包,通过JAR包来发布应用程序。
如果开发者把整个应用制作成一个可执行的JAR交给用户,那么用户使用起来就方便了。在Windows下安装JRE的时候,安装文件会将*.jar文具爱安映射成哟javaw.exe打开。对于一个可执行的JAR包,只要双击打开就好了。
创建可执行JAR包的关键就是让javaw命令知道JAR包中哪个类是主类,javaw命令可以通过运行这个主类来运行程序,这就需要借助清单文件,需要在清单文件中加Main-Class:test.Test
指定JAR包中朱磊是test.Test,javaw命令指定运行该JAR包是从test.Test开始运行。
-
运行JAR包有两种方式:
-
- 使用java命令,使用Java命令运行时的语法是:
java -jar test.jar
。
- 使用java命令,使用Java命令运行时的语法是:
-
- 使用javaw命令,语法:
javaw test.jar
。
- 使用javaw命令,语法:
Java除了生成JAR包,这种压缩包之外,还可以生成WAR包和EAR包。其中WAR文件时Web Archive File,它对应一个web应用得文档;而EAR文件就是Enterprise Archive File,它对应一个企业应用文档(通常由Web应用和EJB两个部分组成)。实际上,WAR和EAR的压缩合适和压缩方式与压缩JAR包完全一样,只是后缀不同。