Android 开发必备 - Java知识点总结

对象

对象的生成与DCL失效问题

在Java中生成一个对象很简单,如下:

Person p = new Person();

简单的一行代码实际在背后执行的了三个操作:

a)给实例分配内存;

b)调用构造函数,初始化成员字段;

c)将p对象指向分配的内存空间(此时p不为null了);

由于Java编译器允许“指令重排序”,因此第二步和第三步可以不按序执行,也就是执行顺序可以是a -> c -> b。

有些同学好奇,上述和DCL(双重校验锁)失效有什么关系?试想一下,在多线程的环境下,如果DCL中声明的单例对象不添加volatile关键字,可能出现的结果是某些线程读到的对象是一个半成品。所以这也是我们为什么在DCL中为单例对象必须添加volatile关键字的原因,因为volatile可以确保内存可见性。

另外。volatile与Synchronized的区别:

Synchronized - > 内存可见性、原子性

volatile - > 内存可见性

克隆

在Java中,克隆分为深克隆和浅克隆。区别在于一个对象中在克隆的时候,是否存在引用字段。示例:

浅: @Override  
    public Address clone() {// clone()是Object的方法  
        Address address = null;  
        try {  
            address = (Address) super.clone();  
        } catch (CloneNotSupportedException e) {  
            e.printStackTrace();  
        }  
        return address;  
}  
深:@Override  
    public Employee clone(){  
     Employee employee = null;  
     try {  
       employee = (Employee) super.clone();    
       employee.address = address.clone();//对引用类型的域进行克隆  
        } catch (CloneNotSupportedException e) {  
            e.printStackTrace();  
     }  
      return employee;  
    }  

一个对象想要支持克隆,只需实现Cloneable接口 - Cloneable接口是不包含任何方法的!其实这个接口仅仅是一个标志,克隆过程由Object.clone方法完成,并实现Object的clone方法即可。

如上例,其中Address实现了Cloneable接口,Address只包含基本数据类型。Employee[也实现了Cloneable接口]除了包含基本数据类型,还包含了Address对象。

代理

在Java中代理分为静态代理和动态代理,如下:

  • 静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。 
  • 动态代理:在程序运行时,运用反射机制动态创建而成。

静态代理原理很简单,只需实现与被代理类相同的接口即可,这里不再详述。

动态代理原理详见我之前的一篇博客:动态代理[JDK]机制解析,简单概括动态代理的原理就是:代理类由Java为我们自动成成,无需我们操作,生成的代理类继承Proxy类,并实现被代理的接口。我们唯一需要做的就是实现我们自定义的InvocationHandler,因为我们对代理类的方法的调用最终都会被转发给我们实现的InvocationHandler。

==> Proxy + 实现接口; InvocationHandler(体现了“代理原则 ”) + 反射。

对代理类方法的调用最终都被转发给InvocationHandler,通过反射实现方法的调用。This.h.invoke(Object proxy, Method method, Object[] args );

动态代理的实际应用:Retrofit网络库,可以查阅我之前的博客:Retrofit原理探究[源码解析]

对象相关的常见知识点

  • newInstance与new:newInstance方法调用默认的构造器(没有参数的构造器〕初始化新创建的对象。如果这个类没有默认的构造器,就会抛出一个异常
  • getClass、instanceOf:instanceof进行类型检查规则是:你属于该类吗?或者你属于该类的派生类吗?而通过getClass获得类型信息采用==来进行检查是否相等的操作是严格的判断。不会存在继承方面的考虑;
  • equals、hashCode

equals、hashCode是一个很重要的知识点,与hash相关的集合,总会涉及这个知识点。有两个问题必须知道:

a)为什么要重写这两个方法?

想要理解为什么重写,其实只要去了解HashMap的工作原理就可以了。

上图来源网络哈,感谢。

HashMap的工作原理其实就是数据结构中讲过的“链地址法”,何为“链地址法”?其实看过上图就明白了 - 数组 + 链表,数组存储的是一个链表,链表中的元素就是hash函数之后,存在hash冲突的都存在一个链表中的元素。

hashCode -> 计算索引,由于是数组,可以快速定位到数组中的位置。

equals -> 找到位置之后,如何查找指定的元素呢?就是通过equals来查找的。

总结:

①若两个对象相等(equals),那么这两个对象一定有相同的哈希值(hashCode);
②若两个对象的哈希值相同,但这两个对象并不一定相等。--> 由于哈希码在生成的时候产生冲突造成的。
在java的集合中,判断两个对象是否相等的规则是: 
1) 判断两个对象的hashCode是否相等。如果不相等,认为两个对象也不相等,完毕; 如果相等,转入2) 
2) 判断两个对象用equals运算是否相等 。如果不相等,认为两个对象也不相等;如果相等,认为两个对象相等(equals()是判断两个对象是否相等的关键) 
Hash***根据上述原则进行对象是否相等的判断。
-->这两个方法如果不重写就会使用Object默认的方法。比如在HashSet中加入两个Student对象,new Student(1,”zhangsan”),new Student(1,”zhangsan”)。虽然这两个对象长得一样,但是地址肯定是不一样的。所以仍然可以加入到hashSet中的。这与HashSet的定义有悖:无重复元素。因此,必须重写两个方法。

b)如何重写?

public class Person {
    private int age;
    private String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Person person = (Person) o;

        if (age != person.age) return false;
        return name != null ? name.equals(person.name) : person.name == null;
    }

    @Override
    public int hashCode() {
        final int prime = age;
        int result = 1;
        result = result * prime + age;
        result = result * prime + (name != null ? name.hashCode() : 0);
        return result;
    }
}

接口

接口中所有的方法自动为public,因此在接口声明时,可以不用加public,但是类实现的接口时,必须加public;

接口中的属性也就是域,自动被设为public static final ;

接口中不能包含实例和静态方法;

异常分类

我们必须显式的处理(try,catch)非运行异常。而Error和RuntimeException,要么不可控(error)要么应该避免发生。

线程

中断、wait/notify/yield/join、Lock(接口)/Condition/ReentrantLock(lock的子类)等

线程方法名称是否释放同步锁是否需要在同步的代码块中调用方法是否已废弃是否可以被中断
sleep()
wait()
suspend   
resume()   
join()  

详述上述非废弃方法

  • wait与sleep异同

wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁,而wait方法则需要释放锁。简单点理解就是使用sleep方法,线程是处于阻塞状态的,而调用wait方法,线程会在被唤醒后重新进入就绪状态竞争CPU资源。

  • notify/notifyAll

也是由Object类提供,他们是和wait()方法配套使用的,同一对象上去调用notify/notifyAll方法,就可以唤醒对应等待的线程了。例如:在main()函数里调用 object.wait()(可以调用wait()的前提是main函数获得了object的锁),则main()函数的当前流程被block,并且main函数失去了object上的锁。直到另外一个线程t调用object.notify()或object.notifyAll()时(这个线程t可以调用notify()和notifyAll()的前提也是具有object锁),这时main函数的线程才有可能成为“可运行”状态的。

  • yield方法

yield方法的作用是暂停当前线程,以便其他线程有机会执行。不能保证当前线程马上停止。yield方法只是将线程转变为就绪状态。线程在调用yield方法后,不需要被唤醒,而是直接进入就绪参加CPU资源竞争。 

  • join方法

等待调用该方法的线程执行完毕后再往下继续执行。

  • Condition

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。[ArrayBlockingQueue:数组 + ReentrantLock{ Condition }]
Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 
 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
A)Conditon中的await()对应Object的wait();

B)Condition中的signal()对应Object的notify();

C)Condition中的signalAll()对应Object的notifyAll()。

  • 线程池简单原理

A)如果线程池中的线程数未达到核心线程数,则创建核心线程来执行任务;

B)如果线程数大于或等于核心线程数,则将任务加入任务队列,线程池中的空闲线程会不断的从任务队列中取出任务进行处理;

C)如果任务队列满了,并且线程数并未达到最大线程数,则创建非核心线程来处理任务;

D)如果线程数超过了最大线程数,则执行饱和策略;

  • 关于ReentrantLock

ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。

一般使用的时候遵循如下用法:

private Lock lock = new ReentrantLock();

public int methodName(){

lock.lock();

try{

......

return ...;

}finally{

lock.unlock();

}
}

当使用lock对象的时候,按照上述惯用法是很重要的。注意:return语句必须在try语句块中出现,以确保unlock不会过早发生,从而将数据暴露给第二个任务。

优势:ReentrantLock允许你尝试着获取锁(tryLock(...))但最终未获取锁,这样如果其他人已经获取了这个锁,那你就可以决定离开去执行其他一些事情,而不是等待直至这个锁释放。

Lock的使用必须显式的释放锁,否则就是一个定时炸弹,这也是其无法替代Synchronized的原因。Lock的出现不是为了替换Synchronized,而是Synchronized无法满足需求时候的一种高级用法。

记住:使用wait和notifyall解决线程间 的协作是一种非常低级的方式。

JVM相关

HotSpot JVM 虚拟机运行时数据区

线程共享:方法区、堆;

线程私有:虚拟机栈(栈帧:局部变量表[也就是我们常说的栈 - 基本数据类型、引用 ]、操作数栈[执行过程中产生的中间值或最终结果值]、方法出口等信息,一个方法的调用与执行完成伴随着栈帧的入栈和出栈)、本地方法栈、程序计数器;

方法区[永久代 - HotSpot]:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。该区域内存回收目标主要是针对常量池的回收和对类型的卸载。<-- 载入Class的地

常量池

字符串常量池

位置:JDK 6.0及之前位于方法区,之后位于堆上。通过Hash表(StringTable)实现。

内容:6.0及之前版本,字符串常量。6.0之后,除了字符串常量,还可以存储字符串对象的引用。

Class常量池

  1. 我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)
  2. 每个class文件都有一个class常量池。
  3. 字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
  4. 符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。-->在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中。

运行时常量池

  1. 运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用
  2. 解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

分析对象是否可回收

引用计数法、可达性分析法;

垃圾收集算法

标记清除算法:- 内存碎片较多

分为标记、清除两个阶段,标记所有需要回收的对象,在标记完成之后统一回收所有被标记的对象。

复制算法[新生代]- 需要分配担保

将可用内存分为两个大小相等的两块,每次只使用其中的一块。当这一块内存使用完了,就将还存活的对象复制到另一块上面,然后再把使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收。

【现在的商业虚拟机都采用这种收集算法来回收新生代[老年代不使用这种算法,因为需要分配担保],IBM研究表明新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%被浪费掉。当Survivor空间不够的时候,需要依赖其他内存(这里指老年代)进行分配担保,这里所谓的担保就是当另一块Survivor空间不足以存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代】

标记-整理算法[老年代]

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清除,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。

分代收集算法:

当前商业虚拟机的垃圾收集都采用“分代收集”算法。

一般Java堆分为新生代和老年代。

新生代:在新生代中,每次垃圾收集都会发现大批对象死去,只有少量存活,因此使用复制算法。

老年代:对象的存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法进行回收。

对象的分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Monitor GC。【新生代的总可用空间是Eden区 + 一个Survivor区的总容量】 - 另外一个Survivor作为轮换备胎

分析:上述代码中,新生代可用内存是9MB(Eden:8M + Survivor:1MB),由于新生代采用的是复制算法,因此在分配allocation1,allocation2,allocation3的时候都没有什么问题,空间足够。但是,在分配allocation4的时候,新生代只剩下了3MB,不足以容下4MB,因此发生了Monitor GC,GC期间虚拟机发现已有的三个2MB大小的对象全部无法放入另一块Survivor空间(只有1MB),所以通过分配担保机制提前转移到老年代。

这次GC结束之后,4MB的allocation4对象顺利的分配到了新生代Eden区中,因此程序运行完的结果是Eden占用4MB,Survivor空闲,老年代被占用了6MB。

GC分类

Monitor GC:发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Monitor GC非常频繁,一般回收速度也比较快。

Major/Full GC:发生在老年代的GC,出现了Major GC,经常会伴随着至少一次的Monitor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC 的速度一般要比Monitor GC慢10倍以上。

如何判定哪些对象进入老年代?

1、大对象【需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组】直接进入老年代;

2、长期存活的对象

虚拟机为每一个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Monitor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象的年龄设为1。对象在Survivor区中每“熬过”一次Monitor GC,年龄就增加一岁,当它的年龄增加到一定程度(默认15岁),就将会晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

3、动态对象年龄判定

如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或者等于改年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

Java中触发初始化的时机

1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。

      生成这四条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段的时候、以及调用一个类的静态方法的时候。

2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有过初始化,则需要先触发其初始化。

3)当初始化一个类的时候,如果发现其父类还没有初始化,则需要先触发其父类的初始化。

4)当虚拟机启动时,用户要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putstatic、REF_invokestatic的方法句柄,并且这个方法句柄对应的类进行过没有初始化,则需要先触发其初始化。

这5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方法都不会触发其初始化,称为被动引用。

类加载机制 - 双亲委派模型

    对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

    双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里的父子关系不是以继承的关系来实现的,而是使用组合关系来复用父加载器的代码。

    工作过程:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求的时候(它的搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。

    为什么要使用这种双亲委派模型?如果没有使用双亲委托模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并存放在了用户的ClassPath中,那系统中将出现多个不同的Object,这样会一片混乱。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值