Java高阶语法

1. 语言基础

基础部分的重点:

1)关键字:static, final, transient, volatile。

2)高阶语法:内部类、泛型。

具体解释:

1)static: static变量不会被GC回收,也就意味着有内存泄露的风险。— 融云的项目还会造成频繁的Full GC.

static会将所引用的属性、方法、内部类,与类直接产生引用关系,而不是与类的实例。为什么一个没有被static修饰的内部类,必须要这么声明:
OuterClass.InnerClass innerClass = new OuterClass().new InnerClass();
这是因为,没有用static修饰内部类InnerClass,所以必须new一个外部类OuterClass的实例,才能在此基础上new出新的内部类的实例。如果使用了static修饰内部类,那么就不用new OuterClass()了,而仅需要这样:
OuterClass.InnerClass innerClass = new OuterClass.InnerClass();

static表示“全局”或者“静态”,用于修饰成员变量或方法,也可形成static代码块。

static修饰的成员变量或方法,为类的所有实例所共享。只要这个类被加载,JVM就可以根据类名,在运行时数据区的Method Area内存区域内找到他们。

<1> static成员变量
static变量在内存中只有一份拷贝,即JVM只为静态变量分配一次内存,在加载类的过程中完成内存分配,可直接用类名访问。而普通成员变量会根据实例数目在内存中有多个拷贝。

<2> static方法
为了方便调用,通常把工具方法声明为static。
static方法必须有方法体,不能是abstract方法。
static方法有下面几个限制:
仅能调用static方法,不能调用普通方法
仅能访问static数据,不能访问普通数据
不能访问this或super

<3> static代码块
在类加载时,static代码块会按顺序加载如果有多个代码块,且static代码块只执行一次。

<4> static和final一起修饰
通常,可将缓存对象设置为static final的

2) volatile:  可见性(主存和工作内存)、不能保证原子性(如i++)
用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新值。即用volatile修饰的变量,一旦写入完成,任何访问这个字段的线程都会得到最新的值。

volatile无法实现原子性,也就是不能用它来做计数器。

为什么volatile用作计数器会有问题呢?
每一个线程都有一个线程栈,线程栈中保存了线程运行时的变量值。当线程访问一个对象的成员变量的时候:
Step1(read and load, 从主存复制到当前工作内存). 通过对象的引用找到对应在堆内存中的变量的值,然后copy到线程栈的内存中。
Step2(use and assign, 执行代码,改变共享变量值). 在线程栈中修改该变量
Step3. (store and write, ) 在修改完之后,线程退出前,自动把线程栈中的变量副本回写到对象的堆内存中。这样,堆中的对象就变化了。
对于volatile修饰的变量,JVM只保证Step1即从主存储加载到线程工作内存的值是最新的。例如,某个时刻thread1, thread2, 获取主内存中的count值都是5,那么,在他们经过step6之后,回写时,只都是6,所以计数器会不准确。

正确使用volatile的条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中

为什么使用volatile?
volatile使用简单,且读操作开销低(几乎和非volatile一样,但volatile的写操作开销很高,但比锁的开销低)
volatile不会像锁一样造成阻塞。如果读操作次数远超过写操作,那么与锁相比,volatile变量会比锁减少很多同步的开销性能。

volatile的应用场景:
场景1. 将volatile作为状态标志使用。如:
volatile boolean shutdownRequested;

...

public void shutdown() { shutdownRequested = true; }

public void doWork() {
    while (!shutdownRequested) {
        // do stuff
    }
}

场景2. 单例

3) AtomicInteger: 基于volatile实现的支持原子性的计数器
之所以可以用作计数器,是因为在+1操作时这样实现的:
public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }
这样,即实现了线程安全的计数器。
例如:
public class AtomicIntegerTest {

private volatile AtomicInteger counter = new AtomicInteger(0);

public static void main(String[] args) {
AtomicIntegerTest t = new AtomicIntegerTest();
t.test();
}

public void test() {
for (int i = 0; i < 100000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
counter.incrementAndGet();
}
}).start();
}

System.out.println("counter:" + counter);
}
}

输出结果为:counter:100000

总结:volatile修饰的成员变量,在每次被线程访问时,都强迫从主内存中重读该成员变量的值;当成员变量发生变化时,强迫线程将变化的值回写到主内存。这样,在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

4)transient: 只能用于修饰变量,不能修饰类或方法
当对象被序列化(写入字节序列到目标文件)时,transient修饰的实例变量不会被持久化;当对象被反序列化时(从源文件读取字节序列进行数据恢复),这样的实例变量也不会被持久化和恢复。也就是说,一个类中有一个transient的成员变量,这个类在经过序列化再反序列化之后,该transient变量是无法被恢复的。

transient的应用场景为,在对对象序列化时将不能被持久化存储的变量声明为transient,比如密码,这样在对象序列化再反序列化之后,transient的成员变量password的值将会消失。

5)final
final可用于修饰成员变量、局部变量、方法、类。
一旦将引用声明为final, 你将不能改变这个引用的指向了。

<1> final变量:变量不可改变。对于final和static一起修饰的变量叫做常量。final变量是只读的。
例如:public static final String NAME = “John”;
NAME = new String(“Jack”); // 此时会编译报错。
final变量存在Stack内存区,但相比普通变量,final变量会被JVM优化;final常量存在Method Area内存区中的常量池。

<2> final方法:不能被Override的方法。
final方法比普通方法更快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。
一个final方法被调用时会转为内嵌调用,不会使用常规的压栈方式,使得运行效率较高。
类中的所有private方法都隐式地指定为是final,所以在继承关系中不存在覆盖问题。

<3> final类:不能被继承。用final修饰的类叫不可变类。String是典型的例子,不可变类的好处是,其对象是只读的,这样在多线程环境下线程安全,不必额外的同步开销。(但前提是final类的成员变量也是final的)。
final类的好处是速度快,因为final类中的方法也都是隐式final的,即不可被override, 这样可在编译期间加载,而不必在运行期间动态加载。
下面是关于final类的好处:
1.不涉及继承和覆盖。
2.其地址引用和装载在编译时完成。
3.在运行时不要求JVM执行因覆盖而产生的动态地址引用而花费时间和空间。
4.与继承链上的一般对象相比,垃圾回收器在收回final对象所占据的地址空间时也相对简单快捷。


使用final关键字的好处:
<1> final变量提高了性能。JVM和Java应用都会缓存final变量 — 这里缓存的仅是final的成员变量,局部变量存到stack中,不会缓存,但JVM会对其进行优化。
<2> final成员变量在多线程场景下线程安全,而不需要加锁等做同步的开销。
<3> 对于final修饰的方法、类,JVM会对其进行自动优化。

对于final的局部变量,存在Stack; 对于final成员变量或者static final变量,存Method Area中的常量池。

关于final的知识点:
<1> final变量必须在声明的时候初始化,或者在构造器中初始化,否则会报编译错误。final局部变量必须在声明时赋值。
<2> 匿名类中的所有变量必须是final变量。(如new Thread(new Runnable(){…}).start();中的...中的变量)
<3> 接口中的所有变量自动都是final的。
<4> final方法在编译阶段绑定,称为静态绑定(static binding)
<5> 将类、方法、变量声明为final, 都能提升性能。因为JVM就有机会进行评估然后优化。
<6> final变量通常要求全大写

注意一点,final的集合对象只是引用不可更改,但可以向其中添加、删除、修改其元素。例如:
private final List list = new ArrayList();
list.add(“aaa”);

final只对引用的“值”(也即它所指向的那个对象的内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。这很类似==操作符:==操作符只负责引用的“值”相等,至于这个地址所指向的对象内容是否相等,==操作符是不管的。

为什么匿名内部类中的变量必须要求是final的?
如果定义一个匿名内部类,并且希望它使用一个在其外部定的对象,那么编译器会要求其参数引用是final 的。经研究,Java虚拟机的实现方式是,编译器会探测局部内部类中是否有直接使用外部定义变量的情况,如果有访问就会定义一个同类型的变量,然后在构造方法中用外部变量给自己定义的变量赋值。

6) 内部类

内部类就是在类的内部定义一个类。

为什么要使用内部类?
《Thinking in Java》中说:使用内部类最吸引人的原因是:每个内部类都能独立地继承一个实现,所以无论外部类是否已经继承了某个实现,对内部类都没有影响。
也就是说,使用内部类可实现多重继承。

使用内部类还能有如下好处:
<1> 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外部对象的信息相互独立。
<2> 在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
<3> 创建内部对象的时刻,并不依赖于外部类对象的创建。
<4> 内部类并没有令人迷惑的“is-a”关系,它就是一个独立体。
<5> 内部类提供了很好的封装,除了该外部类,其他类都不能访问该内部类。
内部类可以直接访问其外部类:
public class OuterClass {
    private String name ;
    private int age;

    /**省略getter和setter方法**/

    public class InnerClass{
        public InnerClass(){
            name = "chenssy";
            age = 23;
        }

        public void display(){
            System.out.println("name:" + getName() +"   ;age:" + getAge());
        }
    }

    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();
        innerClass.display();
    }
}
--------------
Output:
name:chenssy   ;age:23

可以看到,尽管name和age是外部类的private变量,但在创建内部类对象时,该对象必定会捕获一个指向外部类对象的引用。

内部类是编译时的概念,一旦编译成功后,内部类与他的外部类就属于两个完全不同的类。对于OuterClass外部类和其内部类InnerClass,在编译后,会出现下面两个class文件:OuterClass.class和OuterClass$InnerClass.class

Java的内部类分为以下4种:成员内部类、局部内部类、匿名内部类、静态内部类。

<1> 成员内部类

成员内部类是最普通的内部类,可无限制的访问其外部类的所有成员属性和方法,哪怕是private的。但其外部类要访问内部类的成员属性和方法,就必须通过内部类的实例来访问。

成员内部类中不能存在任何static变量和方法, 但可以存在static final的常量。

<2> 局部内部类

局部内部类是嵌套在方法中或作用域中的内部类。
局部内部类主要用于当解决一个复杂问题是,需要创建一个类类辅助,但又不希望别人可以访问这个类。
局部内部类和成员内部类一样被编译,只是他们的作用域不同,局部内部类只能在该方法或作用域中被使用。

方法内部类举例:
public class Parcel5 {
public Destionation destionation(String str) {
class PDestionation implements Destionation {
private String label;

private PDestionation(String whereTo) {
label = whereTo;
}

public String readLabel() {
return label;
}
}
return new PDestionation(str);
}

public static void main(String[] args) {
Parcel5 parcel5 = new Parcel5();
Destionation d = parcel5.destionation("chenssy");
System.out.println("label:" + d.readLabel());
}

}

interface Destionation {
public String readLabel();
}

输出结果:label:chenssy

作用域内的内部类:
public class Parcel6 {
private void internalTracking(boolean b){
        if(b){
            class TrackingSlip{
                private String id;
                TrackingSlip(String s) {
                    id = s;
                }
                String getSlip(){
                    return id;
                }
            }
            TrackingSlip ts = new TrackingSlip("chenssy");
            System.out.println(ts.getSlip());
        }
    }

    public void track(){
        internalTracking(true);
    }

    public static void main(String[] args) {
        Parcel6 parcel6 = new Parcel6();
        parcel6.track();
    }
}

输出结果:chenssy

<3> 匿名内部类

匿名内部类没有名字。以常用的新建线程为例:
new Thread(new Runnable() {
@Override
public void run() {

}
}).start();

其中红色字体即为匿名内部类。
匿名内部类不过是直接使用new来生成一个对象的引用,当然这个引用是隐式的。
匿名内部类不能是抽象类,所以他必须实现他的抽象父类或接口中的所有抽象方法。
当然可以把上面的代码做这样的拆分:
class MyThread implements Runnable {
@Override
public void run() {

}
}

MyThread myThread = new MyThread();
new Thread(myThread);

二者相比,匿名内部类存在一个缺陷,就是其实例myThread无法重复使用。

匿名内部类是局部内部类的一种。

在给匿名内部类传递参数的时候,若该形参在内部类中需要被使用,那么该形参必须是final的。

例如:
public class OuterClass {
    public void display(final String name,String age){
        class InnerClass{
            void display(){
                System.out.println(name);
            }
        }
    }
}
这是为了怕在外部类中改变name, 但这个变化却不会传递到内部类中。
简单理解就是,拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。

由于匿名内部类没有类名,也就没有构造器,那么匿名内部类怎样做初始化呢?答案是通过代码块。例如:
public InnerClass getInnerClass(final int age,final String name){
        return new InnerClass() {
            int age_ ;
            String name_;
            //构造代码块完成初始化工作
            {
                if(0 < age && age < 200){
                    age_ = age;
                    name_ = name;
                }
            }
            public String getName() {
                return name_;
            }

            public int getAge() {
                return age_;
            }
        };
    }

<4> 静态内部类

使用static修饰的成员内部类叫静态内部类,也叫作嵌套内部类。
静态内部类与成员内部类的区别是,成员内部类在编译完成后会隐含低保存其外部类的引用,但静态内部类确没有,这就意味着静态内部类仅能访问其外部类的static成员变量和方法。

7)泛型

为什么要引入泛型?目的是为了避免一个集合可同时存放多种数据类型,使用时再强制转换。

<1> 泛型类:具有一个或多个类型参数的类。例如:
public class Pair<T, U> {
    private T first;
    private U second;

    public Pair(T first, U second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public U getSecond() {
        return second;
    }

    public void setFirst(T newValue) {
        first = newValue;
    }

    public void setSecond(U newValue) {
        second = newValue;
    }
}

实例化泛型类的时候,我们只需要把类型参数换成具体的类型即可,比如实例化一个Pair<T, U>类我们可以这样:
Pair<String, Integer> pair = new Pair<String, Integer>("abc", 111);

<2> 泛型方法:即带有泛型参数的方法。

泛型方法既可以定义在普通类中,也可以定义在泛型类中。例如:
public class ArrayAlg {
    public static <T> T getMiddle(T[] a) {
        return a[a.length / 2];
    }
}

可以这样使用:
String[] strings = {"aa", "bb", "cc"};
String middle = ArrayAlg.getMiddle(strings);
   
Integer[] arr = { 1, 2, 3 };
int value = ArrayAlg.getMiddle(arr);

<3> 泛型的实现原理

从JVM角度看,并不存在真正的“泛型”的概念。比如对于上述的Pair类,JVM看来(编译后的子界面)它是长这样的:

public class Pair {
    private Object first;
    private Object second;

    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }

    public Object getFirst() {
        return first;
    }

    public Object getSecond() {
        return second;
    }

    public void setFirst(Object newValue) {
        first = newValue;
    }

    public void setSecond(Object newValue) {
        second = newValue;
    }
}

由于JVM中泛型类Pair变为它的raw type, 因而getFirst方法返回的是一个Object对象,从而从编译器的角度看,这个方法返回的是我们实例化类时指定的类型参数的对象。实际上,是编译器帮我们完成了强制类型转换工作。也就是说,编译器会把对Pair泛型类中的getFirst方法的调用转化为两条虚拟机指令:第一条是对raw type方法getFirst的调用,返回一个Object对象;第二条指令把返回的Object对象强制转换为当初我们指定的类型参数类型。

综上,泛型机制实际上是编译器帮我们分担了一些麻烦的工作。一方面使用类型参数,可以告诉编译器在编译时进行类型检查;另一方面,原本需要我们做的强制类型转换的工作也由编译器替我们做了。

猜你喜欢

转载自doudou-001.iteye.com/blog/2360351