关于final

final修饰符,用来修饰类、方法和变量,final修饰的类不能够被继承,修饰的方法可以被继承,重载,但是不能被子类重写(即重新定义)。

(1) final类

被声明为final的类(class)不可以被继承(extends)。

(2) final修饰方法

final修饰的方法可以被子类继承,但是不能被子类修改(重写),声明final方法的主要目的是防止该方法的内容被修改。
如果认为方法足够完整,或者强制子类不允许修改可以用final修饰该方法。另外,final方法是静态绑定的,在编译期就确定属于哪个类,所以final方法比非final方法在调用时会快一些。
下面这段话摘自《Java编程思想》第四版第143页:

“使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。 在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的 Java 版本中,不需要使用 final 方法进行这些优化了。“

(3)final变量

final声明的变量可以是基本类型变量也可以是引用变量,基本类型如:int,double,boolean等,被final修饰的基本类型的变量的值在编译时候就已经确定了它的确定值,换句话说就是提前知道了此变量的内容到底是个啥,相当于一个编译期常量。引用类型有封装类型和class声明的类(其实类也是封装),封装类型如:Long,Integer,LocalDateTime,BooK等。
被声明为final的对象的引用不能指向不同的对象,不同的对象在内存中地址是不同的。但是final对象里的数据可以被改变,即使改变了对象地址中某个属性(其他引用)的值并没有影响到这个类实例化出来的对象在内存中数据。
也就是说final对象的引用不能改变,但是里面的值可以改变。比如:

  1. final Item item = Item.ofItemName("heihei", "meimei");
  2. //编译器不会报错
  3. item.setCreateTime(LocalDateTime.now()).setItemName("meimeis");

但是,如果是基本类型和不可变类型被修改了或者被指向了不同的对象,在编译时就不会通过,因为此时对象的引用已经改变了!

  1. //编译失败,不可修改 -
  2. final String str = "aaa";
  3. str = "bbb";
  4. //封装类型 +
  5. final Integer integer = new Integer("32");
  6. integer = 56;
  7. //编译期提示失败,BigDecimal类被class修饰是不可变类型 -
  8. final BigDecimal decimal = new BigDecimal(150);
  9. decimal = new BigDecimal("56");
  10. //可以修改引用类型内部的元素 +
  11. final Item item = new Item();
  12. item.setId(25L);
  13. //使对象指向了不同的内存地址 -
  14. item = new Item();

小结

  • final方法比非final快一些
  • final关键字提高了性能。JVM和Java应用都会缓存final变量。
  • final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
  • 使用final关键字,JVM会对方法、变量及类进行优化。

this关键字

  1. 如果访问本类方法时,一定要加上this关键字(虽然不加也不会报错)
  2. 如果要调用本类的的其他构造方式时,那么必须使用this的形式完成。
  3. 还有一个便是当前对象与其他对象比较时
  4. 出现对象比较的操作之中。
  5. 其他的情况出现最多的链表、树等数据结构的实现上。

this更多的情况是出现在一些设计里面,如果你只是根据业务的开发框架开发代码,那么这个this表示当前对象这一概念的出现很少的。

volatile修饰符

volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。一个volatile对象引用可能是null。

集合与线程安全

在集合框架中,有些类是线程安全的,这些都是jdk1.1中的出现的。在jdk1.2之后,就出现许许多多非线程安全的类。 下面是这些线程安全的同步的类:

  • vector:比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
  • statck:堆栈类,继承了Vector
  • hashtable:比hashmap多了个线程安全
  • enumeration:枚举,相当于迭代器
  • 除了这些之外,其他的都是非线程安全的类和接口。比如常用的ArrayList、HashMap都是线程不安全的。

StringBuffer 和 StringBuilder

和String类不同,StringBuffer和StringBuilder类的对象能够被多次的修改,并且不产生新的未使用对象。

StringBuilder类和StringBuffer之间的最大不同在于StringBuilder的方法不是线程安全的(不能同步访问)。

由于StringBuilder相较于StringBuffer有速度优势,所以多数情况下建议使用StringBuilder类。然而在应用程序要求线程安全的情况下,则必须使用StringBuffer类。

可变参数

JDK 1.5 开始,Java支持传递同类型的可变参数给一个方法。

  • 一个方法中只能指定一个可变参数
  • 它必须是方法的最后一个参数
  • 任何普通的参数必须在它之前声明
  1. //正确
  2. public void getStr(String ...str){
  3. // ...
  4. }
  5. //正确
  6. public void getStr2(int a, String ...str){
  7. // ...
  8. }
  9. //错误
  10. public void getStr2(String ...str, int a){
  11. // ...
  12. }

UTF-8和GBK编码转换

下面哪段程序能够正确的实现了GBK编码字节流到UTF-8编码字节流的转换:byte[] src,dst;

  • A、dst=String.fromBytes(src,”GBK”).getBytes(“UTF-8”)
  • B、dst=new String(src,”GBK”).getBytes(“UTF-8”)
  • C、dst=new String(“GBK”,src).getBytes()
  • D、dst=String.encode(String.decode(src,”GBK”)),”UTF-8” )

正确答案:B
操作步骤就是先解码再编码,先通过GBK编码还原字符串,在该字符串正确的基础上得到“UTF-8”所对应的字节串。

访问控制修饰符

  1. 修饰符说明private私有的,在同一类内可见。
  2. 默认没有声明引用时在同一包(包括子类和非子类)内可见。默认不使用任何修饰符。
  3. protected受保护的,对同一包内的类和所有子类可见。
  4. public共有的,对所有类可见。

主要是默认和protected这两个修饰符,总结起来就是:

  • 默认的:同一包下可访问;
  • protected:同一包和所有子类可访问;

(1)这里的可见、可访问指的是能不能通过 "类的对象.变量名"的方式访问,这是因为除static声明的变量属于类变量外,其他的都属于实例变量,是属于某个对象的!
如,item.createTime直接访问createTime变量,对于那些私有的变量,很多情况下会对外提供public的setter和getter方法来供外部访问。
(2)要注意的是,对于有继承关系的子类来说,比如 class A extends B,A直接继承拥有了默认的(在同一包下)、protected、public的这个字段,可以直接使用该字段,而不用通过再次的实例化父类“父类对象.字段”的形式访问,因为在实例化A类的时候父类B已经实例化好了。特别的,对于protected来说,如下形式是编译不能通过的。

  1. package net.gaox.b
  2. public class B{
  3. protected String age = "20";
  4. public String birthday = "1995";
  5. }
  1. package net.gaox.a
  2. public class A extends B{
  3. public void see(){
  4. B b = new B();
  5. //错误!不同包下的子类不能通过实例出来的父类获取protected的变量
  6. String str = b.age;
  7. //正确,A类继承了B,直接拥有了该字段
  8. String str2 = age;
  9. //正确,birthday为public,可以访问
  10. String str3 = b.birthday;
  11. }
  12. }

结论就是:
在上面总结的修饰符的约束前提下,对于非继承类关系,需要使用 “实例变量.变量名的形式访问”(static类静态变量除外);
对于有继承关系的子类来说, 子类直接继承了拥有了默认的(在同一包下)、protected、public的这个字段,可以直接使用该字段。

List遍历时删除的几种方式比较

1. 会报错的删除方式

在Iterator遍历时使用list删除

  1. while(it.hasNext()){
  2. String item = it.next();
  3. list.remove(item);
  4. }

foreach遍历方式中删除

  1. for (String s : list){
  2. list.remove(s);
  3. }

以上都是报java.util.ConcurrentModificationException,某个线程在 Collection 上进行迭代时,通常不允许另一个线性修改该 Collection,因为在这些情况下,迭代的结果是不确定的。
而对于foreach实际上使用的是iterator进行处理的,而iterator是不允许集合在iterator使用期间通过list删除的,也就是第一种方式,也就是说上面两种方式相当于是同一种。

2. 不会报错,但是有可能漏删或不能完全的删除方式

漏删的情况(索引下标)

  1. List<Integer> list = new ArrayList<>(Arrays.asList(1, 3, 5, 5, 8, 9));
  2. for (int i = 0; i < list.size(); i++) {
  3. if (5 == list.get(i)) {
  4. list.remove(i);
  5. }
  6. }
  7. //list结果:1,3,5,8,9。只删除了一个5,还有一个没有完全删除
  8. //删去[2]后,位置[3]的5就补到刚删除的位置,但i的值在增加,于是就错过了我们计划删除的位置

不能完全删除的情况

  1. List<Integer> list = new ArrayList<>(Arrays.asList(1, 3, 5, 5, 8, 9));
  2. for (int i = 0; i < list.size(); i++) {
  3. list.remove(i);
  4. }
  5. //list现在还剩下:3,5,9三个元素
  6. //每次删除一个元素,后一个位置的元素就会补充到刚被删除的位置
  7. //最终结果就是只删除原集合奇数位置的数据

以上两种情况通过for循环遍历删除,都没有正确达到目的,都是因为在remove后list.size()发生了变化(一直在减少),同时后面的元素会往前移动,导致list中的索引index指向的数据有变化。同时我们的for中的i是一直在加大的!

3. List遍历过程中删除元素的推荐做法

还是使用Iterator遍历,但是不用list来remove。如下代码:

  1. List<Integer> list = new ArrayList<>(Arrays.asList(1, 3, 5, 5, 8, 9));
  2. Iterator<Integer> it = list.iterator();
  3. while(it.hasNext()){
  4. Integer item = it.next();
  5. if (5 == item) {
  6. it.remove();
  7. }
  8. }
  9. ///最终list的元素为:1,3,8,9

对于iterator的remove()方法,也有需要我们注意的地方:

  1. 每调用一次iterator.next()方法,只能调用一次remove()方法。
  2. 调用remove()方法前,必须调用过一次next()方法。

Java中的四种引用:强引用、软引用、弱引用、虚引用

四种级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用

引用类型 被垃圾回收的时机 用途 生命周期
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 当内存不足时 对象缓存 内存不足时终止
弱引用 在垃圾回收时 对象缓存 gc运行后终止
虚引用 Unknown Unknown Unknown

1. 强引用(StrongReference)

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。如下的定义方式:

  1. //强引用,在堆中创建了String这个对象,通过栈中的变量str引用这个对象
  2. String str = new String("abc");
  3. //强引用,str2也指向了堆中创建的String对象
  4. String str2 = str;

这两个引用都是强引用.只要存在对堆中String对象的引用,gc就不会回收该对象,如果显式的设置引用str和str2为null,则gc就会认为堆中的String对象已经不存在其他引用了,此时该对象处于可回收的状态,但是到底什么时候回收该对象,取决于gc的算法。

2. 软引用(SoftReference)

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存,比如在Android中经常把Bitmap作为软引用来缓存图片。

  1. String str = new String("abc");
  2. //软引用
  3. SoftReference<String> soft = new SoftReference<String>(str);
  4. str = null;
  5. System.out.println("before gc:" + soft.get());
  6. System.gc();
  7. //被设置为null并且经历过gc,对象不一定被回收
  8. System.out.println("after gc:" + soft.get());

输出结果:

  1. before gc: abc
  2. after gc: abc

不论是强引用、软引用、弱引用或者虚引用都是针对某个对象来说的,当我们某个对象需要设置为软引用时,只需要给该对象套入到软引用对象中即可。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

3. 弱引用(WeakReference)

弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

可以用如下代码来说明过程:

  1. String str = new String("abc");
  2. //弱引用
  3. WeakReference<String> soft = new WeakReference<String>(str);
  4. str = null;
  5. System.out.println("before gc:" + soft.get());
  6. System.gc();
  7. //被设置为null并且经历过gc,对象被回收
  8. System.out.println("after gc:" + soft.get());

输出结果:

  1. before gc :abc
  2. after gc: null

4. 虚引用(PhantomReference)

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾**回收**。虚引用主要用来跟踪对象被垃圾回收的活动,并且无法通过虚引用获得对象。

虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 建立虚引用之后通过get方法返回结果始终为null。

GC 回收软引用和弱引用对象的过程是一样

  1. 首先将软引用或弱引用的referent设置为null,不再引用堆中的对象;
  2. 将堆中的对象new String("abc")设置为可结束的(finalizable);
  3. 当堆中的new String("abc")对象的 finalize() 方法被运行而且该对象占用的内存被释放,str被添加到它的ReferenceQueue中。