Java基础(热点)

1. 说出ArrayList, Vector, LinkedList 的存储性能和特性?
  • ArrayList 采用的是数组形式来保存对象的,这种方式将对象放在连续的位置中,所以最大的缺点就是插入删除时非常麻烦
  • LinkedList 采用的将对象存放在独立的空间中,而且在每个空间中还保存下一个链接的索引 但是缺点就是查找非常麻烦 要丛第一个索引开始
  • ArrayList和Vector都是用数组方式存储数据, 此数组元素数要大于实际的存储空间以便进行元素增加和插入操作, 他们都允许直接用序号索引元素, 但是插入数据元素涉及到元素移动等内存操作 ,所以索引数据快而插入数据慢.
  • Vector使用了sychronized方法(线程安全), 所以在性能上比ArrayList要差些.
  • LinkedList使用双向链表方式存储数据,按序号索引数据需要前向或后向遍历数据,所以索引数据慢, 是插入数据时只需要记录前后项即可, 所以插入的速度快.
2. HashMap的遍历方式详解?
1./* HashMap */  
2.public static void hashMap(){  
3.    Map<String,String> hashMap = new HashMap<String, String>();  
4.      
5.    for(int i=0;i<100000;i++)  
6.        hashMap.put(i+"", i+"v");  
7.      
8.    long time = System.currentTimeMillis();  
9.    System.out.println("==============方式1:通过遍历keySet()遍历HashMap的value");  
10.    Iterator<String> it = hashMap.keySet().iterator();  
11.    while(it.hasNext()){  
12.        hashMap.get(it.next());  
13.        //System.out.println(hashMap.get(it.next()));  
14.    }  
15.    System.out.println("用时:"+(System.currentTimeMillis() - time));  
16.      
17.      
18.    time = System.currentTimeMillis();  
19.    System.out.println("==============方式2:通过遍历values()遍历HashMap的value");  
20.    Collection<String> values = hashMap.values();  
21.    for(Iterator<String> valIt = values.iterator();valIt.hasNext();){  
22.        valIt.next();  
23.    }  
24.    System.out.println("用时:"+(System.currentTimeMillis() - time));  
25.      
26.      
27.    time = System.currentTimeMillis();  
28.    System.out.println("==============方式3:通过entrySet().iterator()遍历HashMap的key和映射的value");  
29.    Iterator<Entry<String, String>> entryIt = hashMap.entrySet().iterator();  
30.    while(entryIt.hasNext()){  
31.        Entry<String, String> entry = entryIt.next();  
32.        entry.getKey();  
33.        entry.getValue();  
34.        //System.out.println("key:"+entry.getKey()+" value:"+entry.getValue());  
35.    }  
36.    System.out.println("用时:"+(System.currentTimeMillis() - time));  
37.  
38.}  

以上代码运行结果

方式1:通过遍历keySet() 遍历HashMap的value
用时:61
方式2通过遍历values() 遍历HashMap的value
用时:7
方式3:通过entrySet().iterator()遍历HashMap的key和映射的value
用时:12

  • 第一种方式是遍历key,根据key获取映射的vlaue,需要调用get()方法十万次,肯定是效率不高的。建议在数据量较大时不用此方式遍历hashMap。
  • 第二种方式是获取集合中的values,遍历value。但是在遍历value的时候,获取不到key。建议在只需要获取集合中的value时使用此方式。
  • 第三种方式是获取Entry<K,V>类型的Set集合,遍历这个集合,获取每一个Entry<K,V>,通过getKey()和getValue来获取key和value。Entry<K,V>是HashMap集合中的键值对。这样就就相当于遍历了一遍HashMap中的键值对 。省去了第一种方式中get()的操作。建议多用此方式来遍历hashMap结合。

public Set keySet() 方法返回值是Map中key值的集合;public Set<Map.Entry<K,V>> entrySet()方法返回值也是返回一个Set集合,此集合的类型为Map.Entry。
Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示Map中的一个实体(一个key-value对)。接口中有getKey(),getValue方法。

HashMap是这样,换成TreeMap道理也一样。

  • 再来说一下Map.Entry接口的使用场合:

    为Map这个类没有继承Iterable接口所以不能直接通过map.iterator来遍历(list,set就是实现了这个接口,所 以可以直接这样遍历), 所以就只能先转化为set类型,用entrySet()方法,其中set中的每一个元素值就是map中的 一个键值对,也就是Map.Entry<K,V>了,然后就可以遍历了。
    基本上 就是遍历map的时候才用得着它吧。

3. HashMap ,LinkedMap,TreeMap 的区别
  • Hashmap 是一个最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。

  • Hashtable与 HashMap类似,它继承自Dictionary类,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了 Hashtable在写入时会比较慢。

  • LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。

  • TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。

    一般情况下,我们用的最多的是HashMap,HashMap里面存入的键值对在取出的时候是随机的,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map 中插入、删除和定位元素,HashMap 是最好的选择。

  • LinkedHashMap 是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列,像连接池中可以应用。

    TreeMap取出来的是排序后的键值对。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。

4. Hashmap底层,存储因子,hashtable区别,怎么可以变成线程安全的?
  • Key为null总放在数组第一个位置
  • Hashmap底层是由数组和链表组成的,实际上是一个静态内部类entry的数组,key,value就是存储在entry中的,entry还存储了一个指向自身的next指针,当存储元素时,会计算元素的哈希值并对数组长度取模得到一个int值,这个值就是元素要存储在数组中的位置,如果不同元素计算的存储位置相同,则会将新添加进来的entry存在数组中,并将其next指向之前的entry,形成一个链表来解决hash冲突问题;当要根据key查询元素时,会根据同样方法算出索引位置,然后迭代链表,调用equals方法判断key的相等性,如果返回true返回当前entry的value,否则返回null
  • 存储因子:0.75,元素个数超过容量的0.75倍扩容
  • 区别
    HashTable基于Dictionary类,而HashMap是基于AbstractMap
    HashMap的key和value都允许为null,而Hashtable的key和value都不允许为null
    Hashtable是同步的,而HashMap是非同步的
  • 怎么实现线程安全:
    使用hashtable:hashtable的put,get方法都加了同步关键字synchronized,当一个线程在调用该put方法时,其他线程就会被阻塞,且连get方法也不能用,效率低,锁粒度大
    使用ConcurrentHashMap:它包含一个segment数组,将数据分段存储,给每一段数据配一把锁(锁分段),锁粒度小,既安全又高效
    创建一个类实现map接口,重写方法:在每个方法内部对有安全问题的代码块加锁
  • 为什么线程不安全(原因解释很复杂,大概理解一下):
    存在多个线程同时对map扩容时会导致最终只有一个线程扩容后的数组会赋给table,其他线程的数据可能丢失
    使用Collections类的synchronizedMap方法包装一下。方法如下:
    public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) 返回由指定映射支持的同步(线程安全的)映射
5. HashSet和TreeSet有什么区别 ?
  • TreeSet 是二叉树实现的,Treeset中的数据是自动排好序的,不允许放入null值。 TreeSet是SortedSet接口的唯一实现类,向TreeSet中加入的应该是同一个类的对象。
  • HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束
  • HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的 String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例 。
6. HashSet 内部是如何工作的
  • HashSet 的实现其实非常简单,它只是封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。
7. Set里的元素是不能重复的,那么用什么方法来区分重复与否呢? 是用==还是equals()? 它们有和区别 ?
  • 当使用HashSet时,hashCode方法就会得到调用,判断已经存储在集合中的对象的hash code值是否与增加的对象的hash code值一致:

    \1. 如果不一致,直接加进去;

    \2. 如果一致,再进行equals方法的比较,equals如果返回true,表示对象已经加进去了,就不会再增加新的对象;否则加进去。

8. Set保证元素唯一的底层依赖的两个方法?
  • hashCode和equals来完成的

    如果元素的hashCode值相同,才会判断equals是否为true

    如果hashCode的值不同,不会调用equals方法

    注意:对于判断元素是 否存在,以及删除等操作。依赖的方法是元素的hashCode和equals方法。

9. TreeSet排序解释?
  • A:自然排序:要在自定义类中实现Comparerable接口 ,并且重写compareTo方法
  • B:比较器排序:在自定义类中实现Comparetor接口,重写compare方法
10. Hashcode的作用 ?

hashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable。

  • 为什么这么说呢?考虑一种情况,当向集合中插入对象时,如何判别在集合中是否已经存在该对象了?(注意:集合中不允许重复的元素存在)

也许大多数人都会想到调用equals方法来逐个进行比较,这个方法确实可行。但是如果集合中已经存在一万条数据或者更多的数据,如果采用 equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的 hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode 值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题。这样一来实际调用 equals方法的次数就大大降低了,说通俗一点:Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的 字段等)映射成一个数值,这个数值称作为散列值。

put方法是用来向HashMap中添加新的元素,从put方法的具体实现可知,会先调用hashCode方法得到该元素的hashCode 值,然后查看table中是否存在该hashCode值,如果存在则调用equals方法重新确定是否存在该元素,如果存在,则更新value值,否则将 新的元素添加到HashMap中。从这里可以看出,hashCode方法的存在是为了减少equals方法的调用次数,从而提高程序效率。

如果对于hash表这个数据结构的朋友不清楚,可以参考http://www.cnblogs.com/lchzls/p/6714079.html

有些朋友误以为默认情况下,hashCode返回的就是对象的存储地址,事实上这种看法是不全面的,确实有些JVM在实现时是直接返回对象的存储地址,但是大多时候并不是这样,只能说可能存储地址有一定关联

  • 可以直接根据hashcode值判断两个对象是否相等吗?

肯定是 不可以的,因为不同的对象可能会生成相同的hashcode值。虽然不能根据hashcode值判断两个对象是否相等,但是可以直接根据hashcode 值判断两个对象不等,如果两个对象的hashcode值不等,则必定是两个不同的对象。如果要判断两个对象是否真正相等,必须通过equals方法。

也就是说对于两个对象,

1.如果调用equals方法得到的结果为true,则两个对象的hashcode值必定相等;

2.如果equals方法得到的结果为false,则两个对象的hashcode值不一定不同;

3.如果两个对象的hashcode值不等,则equals方法得到的结果必定为false;

4.如果两个对象的hashcode值相等,则equals方法得到的结果未知。

a.hashCode( ) 有什么用? 与a,equals(b) 有什么关系 ? hashCode( ) 和equals( ) 方法的重要性体现在什么地方 ?

11. a.hashCode( ) 有什么用? 与a,equals(b) 有什么关系 ?
  • 1、hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的;

    2、如果两个对象相同,就是适用于equals(Java.lang.Object) 方法,那么这两个对象的hashCode一定要相同;

    3、如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;

    4、两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。

    再归纳一下就是hashCode是用于查找使用的,而equals是用于比较两个对象的是否相等的。

12. hashCode( ) 和equals( ) 方法的重要性体现在什么地方 ?
  • Java中的HashMap使用hashCode()和equals()方法来确定键值对的索引,当根据键获取值的时候也会用到这两个方法。
    如果没有正确的实现这两个方法,两个不同的键可能会有相同的hash值,因此可能会被集合认为是相等的。
    而且,这两个方法也用来发现重复元素,所以这两个方法的实现对HashMap的精确性和正确性是至关重要的。

    HashMap的很多函数要基于hashCode()方法和equals()方法,hashCode()用来定位要存放的位置,equals()用来判断是否相等。

    • 相等的概念是什么?

    Object的equals()只是简单地判断是不是同一个实例,但是有时候我们想要的是逻辑上的相等。比如一个学生类Student,有一个成员变量studentID,只要StudentID相等,不是同一个实例我们也认为是同一个学生。当我们认为判定equals的相等应该是逻辑上的相等而不是只判断是不是内存中的同一个东西的时候,我们就应该重写equals()。而涉及到HashMap的时候,重写了equals()就要重写hashCode()。

    总结:

    同一个对象(没有发生过修改)无论何时调用hashCode(),得到的返回值必须一样。

    hashCode()返回值相等,对象不一定相等,通过hashCode()和equals()必须能唯一确定一个对象。

    一旦重写了equals(),就必须重写hashCode()。而且hashCode()生成哈希值的依据应该是equals()中用来比较是否相等的字段。如果两个由equals()规定相等的对象生成的hashCode不等,对于HashMap来说,他们可能分别映射到不同位置,没有调用equals()比较是否相等的机会,两个实际上相等的对象可能被插入到不同位置,出现错误。其他一些基于哈希方法的集合类可能也会有这个问题。

13. List,Set,Map三个接口, 存取元素时各有什么特点
  • List与Set都是单列元素的集合,它们有一个功共同的父接口Collection。

    • Set里面不允许有重复的元素,

    存元素:add方法有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该元素时,则返回true;当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返回结果为false。

    取元素:没法说取第几个,只能以Iterator接口取得所有的元素,再逐一遍历各个元素。

    • List表示有先后顺序的集合,

    存元素:多次调用add(Object)方法时,每次加入的对象按先来后到的顺序排序,也可以插队,即调用add(int index,Object)方法,就可以指定当前对象在集合中的存放位置。

    取元素:方法1:Iterator接口取得所有,逐一遍历各个元素

    	方法2:调用get(index i)来明确说明取第几个。
    
    • Map是双列的集合,存放用put方法:put(obj key,obj value),每次存储时,要存储一对key/value,不能存储重复的key,这个重复的规则也是按equals比较相等。

    取元素:用get(Object key)方法根据key获得相应的value。

        也可以获得所有的key的集合,还可以获得所有的value的集合,
    
        还可以获得key和value组合成的Map.Entry对象的集合。
    

    List以特定次序来持有元素,可有重复元素。Set 无法拥有重复元素,内部排序。Map 保存key-value值,value可多值。

14. List,Set,Map三个接口的 各自子类的存取速度以及线程安全 ?
  • List接口有三个实现类:LinkedList,ArrayList,Vector
    LinkedList:底层基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还存储下一个元素的地址。链表增删快,查找慢
    ArrayList和Vector的区别:ArrayList是非线程安全的,效率高;Vector是基于线程安全的,效率低

  • Set接口有两个实现类:HashSet(底层由HashMap实现),LinkedHashSet
    SortedSet接口有一个实现类:TreeSet(底层由平衡二叉树实现)
    Query接口有一个实现类:LinkList

  • Map接口有三个实现类:HashMap,HashTable,LinkeHashMap
    HashMap非线程安全,高效,支持null;
    HashTable线程安全,低效,不支持null
    SortedMap有一个实现类:TreeMap

    • HashMap:Map的实现类,缺省情况下是非同步的,可以通过Map Collections.synchronizedMap(Map m)来达到线程同步
    • HashTable:Dictionary的子类,确省是线程同步的。不允许关键字或值为null
    • TreeMap:元素的顺序很重要时选用,当元素不必以特定的顺序进行存储时,使用HashMap。

    Hashtable的使用不被推荐,HashMap提供了所有类似的功能,并且速度更快。当你需要在多线程环境下使用时,HashMap也可以转换为同步的。

    其实最主要的是,list是用来处理序列的,而set是用来处理集的。Map是知道的 ,存储的是键值对
    set 一般无序不重复.map kv 结构 list 有序
    ArrayList遍历元素和随机访问元素的效率比较高。
    LinkedList在于插入,删除元素时效率比较高。
    在ArrayList 之前存在Vector是线程安全的,效率低,长度默认增长一倍。而ArrayList 线程非安全的,重速度,长度增长50%。

    Iterator :为集合而生,遍历集合。隐藏各种集合实现类内部细节,提供遍历集合统一编程接口

15. 遍历一个List有哪些不同的方式?
1.   package com.suwu.listtest;  
2.import java.util.List;  
3.import java.util.ArrayList;  
4.import java.util.Iterator;  
5.public class ListTest  
6.{  
7.    public static void main(String[] args )  
8.    {  
9.      List<Integer> list=new ArrayList<Integer>();  
10.      list.add(new Integer(100));  
11.      list.add(new Integer(200));  
12.      list.add(new Integer(54));  
13.      list.add(new Integer(10242));  
14.      //遍历方式1---while(it.hasNext())  
15.      System.out.println("遍历方式1--while(it.hasNext())");  
16.      Iterator<Integer> it=list.iterator();  
17.      while(it.hasNext())  
18.      {  
19.         System.out.println(it.next());  
20.      }  
21.      //遍历方式2--get(i)  
22.      System.out.println("遍历方式2--get(i)");  
23.      for(int i=0;i<list.size();i++)  
24.      {  
25.        System.out.println(list.get(i));  
26.      }  
27.       //遍历方式3--Object o  
28.      System.out.println("遍历方式3--Object o");  
29.      for(Object o:list)  
30.      {  
31.        System.out.println(o);  
32.      }  
33.    }  
34.}  
16. ArrayList 和HashMap 的默认大小是多少?
  • hashMap为16,ArrayList为10. 但是ArrayList比较特殊,只是初始化了10个空的数组
17. Array 和 ArrayList 有何区别?什么时候更适合用Array

存储内容比较:

· Array数组可以包含基本类型和对象类型,

· ArrayList却只能包含对象类型。

但是需要注意的是:Array数组在存放的时候一定是同种类型的元素。ArrayList就不一定了,因为ArrayList可以存储Object。

空间大小比较:

· 它的空间大小是固定的,空间不够时也不能再次申请,所以需要事前确定合适的空间大小。

· ArrayList的空间是动态增长的,如果空间不够,它会创建一个空间比原空间大一倍的新数组,然后将所有元素复制到新数组中,接着抛弃旧数组。而且,每次添加新的元素的时候都会检查内部数组的空间是否足够。(比较麻烦的地方)。

方法上的比较:
ArrayList作为Array的增强版,当然是在方法上比Array更多样化,比如添加全部addAll()、删除全部removeAll()、返回迭代器iterator()等。

适用场景:
如果想要保存一些在整个程序运行期间都会存在而且不变的数据,我们可以将它们放进一个全局数组里,但是如果我们单纯只是想要以数组的形式保存数据,而不对数据进行增加等操作,只是方便我们进行查找的话,那么,我们就选择ArrayList。而且还有一个地方是必须知道的,就是如果我们需要对元素进行频繁的移动或删除,或者是处理的是超大量的数据,那么,使用ArrayList就真的不是一个好的选择,因为它的效率很低,使用数组进行这样的动作就很麻烦,那么,我们可以考虑选择LinkedList。

**18. ** 一句代码实现list元素去重

List out= new ArrayList(new HashSet(in));

19. Conllections

而Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

Collections是个java.util下的类,它包含有各种有关集合操作的静态方法。

collections 此类完全由在 collection 上进行操作或返回 collection 的静态方法组成。它包含在 collection 上操作的多态算法,即“包装器”,包装器返回由指定 collection 支持的新 collection,以及少数其他内容。 如果为此类的方法所提供的 collection 或类对象为 null,则这些方法都会抛出 NullPointerException。

20. 多线程的优点 缺点 以及多线程的实现方法.
  • 多线程的作用

为了解决负载均衡问题,充分利用CPU资源.为了提高CPU的使用率,采用多线程的方式去同时完成几件事情而不互相干扰.为了处理大量的IO操作时或处理的情况需要花费大量的时间等等,比如:读写文件,视频图像的采集,处理,显示,保存等

  • 多线程的好处:

1.使用线程可以把占据时间长的程序中的任务放到后台去处理

2.用户界面更加吸引人,这样比如用户点击了一个按钮去触发某件事件的处理,可以弹出一个进度条来显示处理的进度

3.程序的运行效率可能会提高

4.在一些等待的任务实现上如用户输入,文件读取和网络收发数据等,线程就比较有用了.

  • 多线程的缺点:

1.如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换.

2.更多的线程需要更多的内存空间

3.线程中止需要考虑对程序运行的影响.

4.通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生

  • 多线程的实现方法.

https://blog.csdn.net/king_kgh/article/details/78213576

这是6种实现方式的介绍

1.继承Thread类,重写run方法

2.实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target

3.通过Callable和FutureTask创建线程

4.通过线程池创建线程

  • 前面两种可以归结为一类:无返回值,原因很简单,通过重写run方法,run方式的返回值是void,所以没有办法返回结果
  • 后面两种可以归结成一类:有返回值,通过Callable接口,就要实现call方法,这个方法的返回值是Object,所以返回的结果可以放在Object对象中
21. ThreadLocal(线程局部变量) 以及它的API

ThreadLocal是什么呢?

  • 其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。

从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。

概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

  • API:

ThreadLocal() 创建一个线程本地变量。

T get() 返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。

protected T initialValue() 返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。

若该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialValue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。

void remove() 移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialValue。

void set(T value) 将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue() 方法来设置线程局部变量的值。程序中一般都重写initialValue方法,以给定一个特定的初始值。

ThreadLocal使用场合主要解决多线程中数据数据因并发产生不一致问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,单大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。

ThreadLocal不能使用原子类型,只能使用Object类型。ThreadLocal的使用比synchronized要简单得多。

ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂。

ThreadLocal使用的一般步骤

1、在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。

2、在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。

3、在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。

22. 同步和异步有何异同,在什么情况下分别使用他们?举例说明

如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。

当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率

  • Java中交互方式分为同步和异步两种:

同步交互:指发送一个请求,需要等待返回,然后才能够发送下一个请求,有个等待过程

异步交互:指发送一个请求,不需要等待返回, 随时可以再发送下一个请求,即不需要等待
区别:一个需要等待,一个不需要等待,在部分情况下,我们的项目开发中都会优先选择不需要等待的异步交互方式。

  • 哪些情况建议使用同步交互呢?

比如银行的转账系统,对数据库的保存操作等等,都会使用同步交互操作,其余情况都优先使用异步交互

23. 请说出与线程同步以及线程调度相关的方法
  • wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
  • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关
  • notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
24. 启动一个线程是调用 run() 还是 start() 方法?start() 和 run() 方法有什么区别

一、区别

Java中启动线程有两种方法,继承Thread类和实现Runnable接口,由于Java无法实现多重继承,所以一般通过实现Runnable接口来创建线程。但是无论哪种方法都可以通过start()和run()方法来启动线程,下面就来介绍一下他们的区别。

start方法:

通过该方法启动线程的同时也创建了一个线程,真正实现了多线程。**无需等待run()方法中的代码执行完毕,就可以接着执行下面的代码。此时start()的这个线程处于就绪状态,当得到CPU的时间片后就会执行其中的run()方法。**这个run()方法包含了要执行的这个线程的内容,run()方法运行结束,此线程也就终止了。

run方法:

通过run方法启动线程其实就是调用一个类中的方法,当作普通的方法的方式调用。并没有创建一个线程,程序中依旧只有一个主线程,必须等到run()方法里面的代码执行完毕,才会继续执行下面的代码,这样就没有达到写线程的目的。

下面我们通过一个很经典的题目来理解一下:

public class Test {
    public static void main(String[] args) {
        Thread t = new Thread(){
            public void run() {
                pong();
            }
        };
        t.run();
        System.out.println("ping");
    }

    static void pong() {
        System.out.println("pong");
    }
}

代码如图所示,那么运行程序,输出的应该是什么呢?没错,输出的是”pong ping”。因为t.run()实际上就是等待执行new Thread里面的run()方法调用pong()完毕后,再继续打印”ping”。它不是真正的线程。

而如果我们将t.run();修改为t.start();那么,结果很明显就是”ping pong”,因为当执行到此处,创建了一个新的线程t并处于就绪状态,代码继续执行,打印出”ping”。此时,执行完毕。线程t得到CPU的时间片,开始执行,调用pong()方法打印出”pong”。

如果感兴趣,还可以多加几条语句自己看看效果。

二、源码

那么他们本质上的区别在哪里,我们来看一下源码:

/**java
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

可以看到,当一个线程启动的时候,它的状态(threadStatus)被设置为0,如果不为0,则抛出IllegalThreadStateException异常。正常的话,将该线程加入线程组,最后尝试调用start0方法,而start0方法是私有的native方法(Native Method是一个java调用非java代码的接口)。

我猜测这里是用C实现的,看来调用系统底层还是要通过C语言。这也就是为什么start()方法可以实现多线程。而调用run()方法,其实只是调用runnable里面自己实现的run()方法。

我们再看看Thread里run()的源码:

@Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

如果target不为空,则调用target的run()方法,那么target是什么:

/* What will be run. */
    private Runnable target;

其实就是一个Runnable接口,正如上面代码中new Thread的部分,其实我们就是在实现它的run()方法。所以如果直接调用run,就和一个普通的方法没什么区别,是不会创建新的线程的,因为压根就没执行start0方法。

三、实现

前面说了,继承Thread类和实现Runnable接口都可以定义一个线程,那么他们又有什么区别呢?
在《Java核心技术卷1 第9版》第627页提到。可以通过一下代码构建Thread的子类定义一个线程:

class MyThread extends Thread {
    public void run() {
        //do Something
    }
}

然后,实例化一个对象,调用其start方法。不过这个方法不推荐。应该减少需要并行运行的任务数量。如果任务很多,要为每个任务创建一个独立的线程所付出的代价太多,当然可以用线程池来解决。

实现Runnable接口所具有的优势:
1、避免Java单继承的问题
2、适合多线程处理同一资源
3、代码可以被多线程共享,数据独立,很容易实现资源共享

**25. 线程的生命周期

1. 线程的生命周期线程是一个动态执行的过程,它也有一个从产生到死亡的过程。

  • 生命周期的五种状态

**新建(new Thread)**当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
例如:Thread t1=new Thread();

**就绪(runnable)**线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。例如:t1.start();

**运行(running)**线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。

死亡(dead)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。

自然终止:正常运行run()方法后终止

异常终止:调用**stop()**方法让一个线程终止运行

堵塞(blocked)
由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。

正在睡眠:用sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。

正在等待:调用wait()方法。(调用motify()方法回到就绪状态)

被另一个线程所阻塞:调用suspend()方法。(调用resume()方法恢复)

2. 常用方法

void run() 创建该类的子类时必须实现的方法

void start() 开启线程的方法

static void sleep(long t) 释放CPU的执行权,不释放锁

static void sleep(long millis,int nanos)

final void wait()释放CPU的执行权,释放锁

final void notify()

static void yied()可以对当前线程进行临时暂停(让线程将资源释放出来)

3. 结束线程

  1. 结束线程原理:就是让run方法结束。而run方法中通常会定义循环结构,所以只要控制住循环即可
  2. 方法----可以boolean标记的形式完成,只要在某一情况下将标记改变,让循环停止即可让线程结束

(3)public final void join( ) //让线程加入执行,执行某一线程join方法的线程会被冻结,等待某一线程执行结束,该线程才会恢复到可运行状态

4. 临界资源:多个线程间共享的数据称为临界资源

(1)互斥锁

a. 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

b. Java对象默认是可以被多个线程共用的,只是在需要时才启动“互斥锁”机制,成为专用对象。

c. 关键字synchronized用来与对象的互斥锁联系

d. 当某个对象用synchronized修饰时,表明该对象已启动“互斥锁”机制,在任一时刻只能由一个线程访问,即使该线程出现堵塞,该对象的被锁定状态也不会解除,其他线程任不能访问该对象。

26 . 常见的线程池

①newSingleThreadExecutor

单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务

②newFixedThreadExecutor(n)

固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行

③newCacheThreadExecutor(推荐使用)

可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。

④newScheduleThreadExecutor

大小无限制的线程池,支持定时和周期性的执行线程

java提供的线程池更加强大,相信理解线程池的工作原理,看类库中的线程池就不会感到陌生了。

img

img

27. 线程池的作用

线程池作用就是限制系统中执行线程的数量。

根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

28. 线程池原理:

一个线程池中有多个处于可运行状态的线程,当向线程池中添加Runnable或Callable接口对象时,

就会有一个线程来执行run()方法或call()方法。如果方法执行完毕,则该线程并不终止,

而是继续在池中处于可运行状态,以运行新的任务。

29. String不可变的好处

String是不可变的、final的。Java在运行时也保存了一个字符串池(String pool),这使得String成为了一个特别的类。

好处:

1, 便于实现String常量池

只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同 一个字符串。但如果字符串是可变的,那么String interning将不能实现(译者注:String interning是指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串。),因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。

2, 避免网络安全问题

如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。

3, 使多线程安全:

因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。

4, 避免本地安全性问题

类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。

5,加快字符串处理速度

因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

30. String的api

img

31.string,stringbuilder,stringbufferr 的区别**

三者在执行速度方面的比较:StringBuilder > StringBuffer > String

StringBuilder:线程非安全的

StringBuffer:线程安全的

区别在于

StringBufferd支持并发操作,线性安全的,适 合多线程中使用。

StringBuilder不支持并发操作,线性不安全的,不适合多线程中使用。

StringBuilder类不是线程安全的,但其在单线程中的性能比StringBuffer高。

对于三者使用的总结

1.如果要操作少量的数据用 = String

2.单线程操作字符串缓冲区 下操作大量数据 = StringBuilder

3.多线程操作字符串缓冲区 下操作大量数据 = StringBuffer

string 的长度是不可变的;好处:字符串可以共享

StringBuffer的长度是可变的,如果你对字符串中的内容经常进行操作,特别是内容要修改时,那么使用 StringBuffer,如果最后需要转换为String,那么使用 StringBuffer 的 toString() 方法;线程安全;

StringBuilder 是从 JDK 5 开始,为StringBuffer该类补充了一个单个线程使用的等价类;通常应该优先使用 StringBuilder 类,因为它支持所有相同的操作,但由于它不执行同步,所以速度更快。

使用字符串的时候要特别小心,如果对一个字符串要经常改变的话,就一定不要用String,否则会创建许多无用的对象出来

32. 如何判断一个字符串是空值或者空字符串

StringUtils isBlank();

补充:

1**.类型:**
null表示的是一个对象的值,而并不是一个字符串。例如声明一个对象的引用,String a = null ;
**""**表示的是一个空字符串,也就是说它的长度为0。例如声明一个字符串String str = “” ;

2、内存分配
String str = null ; 表示声明一个字符串对象的引用,但指向为null,也就是说还没有指向任何的内存空间;
String str = “”; 表示声明一个字符串类型的引用,其值为**""空字符串,这个str引用指向的是空字符串的内存空间**;

在java中变量和引用变量是存在栈中(stack),而对象(new产生的)都是存放在堆中(heap):

33. 为什么java中的String是不可变的

String是Java中的一个不可变类。所谓不可变,简单来说就是其对象不能被修改。实例中的所有信息在初始化的时候就已经具备,并且不能被修改(老外好啰嗦…)。不可变类有很多优点。这篇文章简要说明了为什么String被设计为不可变类。关于其好的回答应该建立在对内存模型、同步和数据结构等的理解之上。

1. 字符串池的需求

字符串池是一个位于方法区的特殊区域。当一个字符串被创建的时候,如果该字符串已经存在于字符串池中,那么直接返回该字符串的引用,而不是创建一个新的字符串。
下边的代码将只会创建一个字符串对象:

String s1 = “abcd”;String s2 = “abcd”;

也就是s1和s2都指向同一个字符串对象。
如果String不是不可变的,那么修改s1的字符串对象同样也会导致s2的内容发生变化。

2. 缓存Hashcode

字符串的hashcode在Java中经常被用到。例如,在一个HashMap中。其不可变性保证了hashcode(哈希值)总是保持不变,从而不用担心因hashcode变化导致的缓存问题。那就意味着,不用每次在其使用的时候计算其hashcode,从而更加高效。
在String类中,有如下代码:

private int hash; //用来缓存hash code

3. 简化其他对象的使用

为了理解这一点,请看下边的代码:

HashSet<String> set = new HashSet<String>();
set.add(new String("a"));
set.add(new String("b"));
set.add(new String("c"));for (String a : set)
a.value = "a";

这个例子中,如果String是可变的,也就是说set中的值是可变的,这会影响到set的设计(set包含不重复的元素)。当然这个例子是有问题的,在String类中是不存在value这个属性的。

4.安全性

字符串在许多的java类中都用作参数,例如网络连接,打开文件等等。如果字符串是可变的,一个连接或文件就会被修改从而导致严重的错误。可变的字符串也会导致在使用反射时导致严重的问题,因为参数是字符串形式的。
举例如下:

boolean connect(String s) {
if (!isSecure(s)) {
    throw new SecurityException();
}
// 如果s内的值被修改,则会导致出现问题
doSomethind(s);  
}

(虽然略牵强,但是也有一定道理)

5. 不可变的对象本身就是线程安全的

不可变的对象,可以在多个线程间自由共享。从而免除了进行同步的麻烦。

总之, String被设计为不可变的类,是出于性能和安全性的考虑,这也是其他所有不可变类应用的初衷

34. 对象创建过程

JVM会先去方法区下找有没有所创建对象的类存在,有就可以创建对象了,没有则把该类加载到方法区

在创建类的对象时,首先会先去堆内存中分配空间

当空间分配完后,加载对象中所有的非静态成员变量到该空间下

所有的非静态成员变量加载完成之后,对所有的非静态成员进行默认初始化

所有的非静态成员默认初始化完成之后,调用相应的构造方法到栈中

在栈中执行构造函数时,先执行隐式,再执行构造方法中书写的代码

执行顺序:静态代码库,构造代码块,构造方法

当整个构造方法全部执行完,此对象创建完成,并把堆内存中分配的空间地址赋给对象名(此时对象名就指向了该空间)

35. 类加载器分类

从Java虚拟机的角度来讲,只存在两种不同的类加载器:

  • 一种是启动类加载器**(Bootstrap ClassLoader)**,这个类加载器是虚拟机的一部分;

  • 另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机,并且全部继承自java.lang.ClassLoader。细分来看,类加载器还可以分为如下几类:

    1.启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库。(用来加载java核心类库,无法被java程序直接引用。)
     2.扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。(负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包)
     3.应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器
     
     4.Custom ClassLoader/用户自定义类加载器(通过继承 java.lang.ClassLoader类的方式实现。)
    

属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader

1 Bootstrap ClassLoader

Bootstrap ClassLoader被称为根类加载器,它负责加载Java的核心类。根类加载器并不是java.lang.ClassLoader的子类,而是由JVM自身实现的。在Sun的JVM中,当执行java.exe命令时,使用-Xbootclasspath选择或使用-D选项指定sun.boot.class.path系统属性值可以指定加载附加的类。

通过如下程序查看根类加载器加载的类的路径:

\00001.

public class LoaderTest {

public static void main(String[] args) throws IOException {

    // 获取根类加载器所加载的全部URL数组

    URL[] urls = Launcher.getBootstrapClassPath().getURLs();

    // 遍历、输出根类加载器加载的全部URL

    System.out.println("*********根类加载器加载的全部URL*************");

    for (URL url : urls) {

        System.out.println(url.toExternalForm());

    }

}

}

输出结果:

\00001.

根类加载器加载的全部URL****

file:/D:/Java/jdk1.7.0_80/jre/lib/resources.jar

file:/D:/Java/jdk1.7.0_80/jre/lib/rt.jar

file:/D:/Java/jdk1.7.0_80/jre/lib/sunrsasign.jar

file:/D:/Java/jdk1.7.0_80/jre/lib/jsse.jar

file:/D:/Java/jdk1.7.0_80/jre/lib/jce.jar

file:/D:/Java/jdk1.7.0_80/jre/lib/charsets.jar

file:/D:/Java/jdk1.7.0_80/jre/lib/jfr.jar

file:/D:/Java/jdk1.7.0_80/jre/classes

\00002.

2 Extension ClassLoader

Extension ClassLoader是扩展类加载器,它负责加载JRE的扩展目录中JAR包的类,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

3 Application ClassLoader

Application ClassLoader是应用程序类加载器,由sun.misc.Launcher$AppClassLoader实现,可以通过ClassLoader.getSystemClassLoader()方法获取,因此也被称为系统类加载器。它负责加载用户类路径(ClassPath)上指定的类库,开发者可以直接使用这个类加载器,如果应用程序没有自定义类加载器,一般情况下这个就是程序中默认的类加载器。

4 自定义类加载器器

为了实现类加载的个性化定制,我们可以通过扩展java.lang.ClassLoader类来实现自定义类加载器,详细的实现随后描述。

我们的应用程序一般都是由这几种类加载器相互配合进行加载的,类加载器之间的关系如图:

img

类加载器之间的关系并非是类继承性质的父子关系,而是一种组合关系。

36. jvm加载class文件

JVM加载Class包括3个阶段:类加载,链接,初始化

1.类加载

JVM通过类的全限定名(包命+类名)找到类的.class文件。然后把这个.class文件加载进来,这个过程需要通过ClassLoader来实现。

ClassLoader包括

Boostrap ClassLoader,

Extendsion ClassLoader

System ClassLoader。

Boostrap ClassLoader:启动类加载器,它用来加载一些jdk的核心类,主要负责JAVA_HOME
/jre/lib下的类的加载,可以通过参数-Xbootclasspath制定需要装入的jar包。 它本身不是用java实现的,所以肯定不是ClassLoader的子类了。

Extendsion ClassLoader:扩展类加载器,用来加载一些扩展类,主要负责JAVA_HOME
/jre/lib/ext下类的加载。此类是ClassLoader的一个子类。

System ClassLoader:系统类加载器 也叫Application ClassLoader。是离我们最近的ClassLoader了,它负责加载CLASSPATH里指定的那些类。我们要实现自己的ClassLoader也是继承自该类。SystemClassLoader的父类是Extension ClassLoader。

类的加载过程分两步:

第一步:从下往上查找类是否已经加载,如果找到,直接返回已加载的类,如果没找着接着往上找。

第二步:如果到Bootstrap ClassLoader还没找到,这时Bootstrap ClassLoader会尝试加载该类,如果成功加载,直接返回加载后的类,如果无法加载,交由Extension ClassLoader去加载,依次类推。如果最后仍然没找到,程序会抛出ClassNotFoundException。

2 链接:

当一个class文件被成功加载后,接下来就要做链接了。链接就是要把二进制的.class文件转换成可以被jvm执行的Class对象的过程。这个过程又分为:检验、准备、解析。

检验:就是检查.class的结构是否正确,是否符合Java虚拟机的语义要求。

准备:包括创建类或接口的静态域以及把这些静态域初始化为标准的缺省值。注意此处的初始化不同于后面的的初始化步骤。如有一个static的String 变量str,我们知道,在JAVA中String变量默认的初始值是null,此处的初始化就是将null赋值给str。

解析:将类中对另一个类或接口的符合引号转化成全限定名引用,将对他们的方法、字段的符合引用转化成直接引用。

3 初始化:执行类或接口中的静态初始化函数(块),将静态变量初始化。这就是我们平时理解的对静态变量赋值。

至此,一个类才加载完成,可以调用类的类变量了(静态变量)和对类进行实例化了。

**37. **JVM内存分为哪几部分,这些部分分别都存储哪些数据?

按照Java虚拟机规范的规定,JVM自动管理的内存将会包括以下几个运行时数据区域
img

38. 类加载过程

Jvm先去方法区下找类是否存在,如果不存在,则把类加载到方法区下

先加载非静态内容到方法区下的非静态区域内

再加载静态内容到方法区下的静态区域内,并对所有的静态成员变量进行默认初始化,再对所有的静态成员变量显式初始化

JVM自动执行静态代码块(静态代码块在栈中执行)[如果有多个静态代码,执行的顺序是按照代码书写的先后顺序执行]

所有的静态代码块执行完成之后,此时类的加载完成

39. 单例模式在项目中的应用

日志,数据库连接池,网站技术,多线程线程池

单例模式有以下特点:
  1、单例类只能有一个实例。
  2、单例类必须自己创建自己的唯一实例。
  3、单例类必须给所有其他对象提供这一实例。
  单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

40. 单例模式有五种写法:懒汉、饿汉、双重检验锁、静态内部类、枚举。

单例模式算是设计模式中最容易理解,也是最容易手写代码的模式了吧。但是其中的坑却不少,所以也常作为面试题来考。本文主要对几种单例写法的整理,并分析其优缺点。很多都是一些老生常谈的问题,但如果你不知道如何创建一个线程安全的单例,不知道什么是双检锁,那这篇文章可能会帮助到你。

懒汉式,线程不安全

当被问到要实现一个单例模式时,很多人的第一反应是写出如下的代码,包括教科书上也是这样教我们的。

public class Singleton {
    private static Singleton instance;
    private Singleton (){}
    public static Singleton getInstance() {
     if (instance == null) {
         instance = new Singleton();
     }
     return instance;
    }
}

这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。

懒汉式,线程安全

为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。

public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。

双重检验锁

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

public static Singleton getSingleton() {
    if (instance == null) {                         //Single Checked
        synchronized (Singleton.class) {
            if (instance == null) {                 //Double Checked
                instance = new Singleton();
            }
        }
    }
    return instance ;
}

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

给 instance 分配内存

调用 Singleton 的构造函数来初始化成员变量

将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

我们只需要将 instance 变量声明成 volatile 就可以了。

public class Singleton {
    private volatile static Singleton instance; //声明成 volatile
    private Singleton (){}

    public static Singleton getSingleton() {
        if (instance == null) {                         
            synchronized (Singleton.class) {
                if (instance == null) {       
                    instance = new Singleton();
                }
            }        
        return instance;
    }
}

有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。

相信你不会喜欢这种复杂又隐含问题的方式,当然我们有更好的实现线程安全的单例模式的办法。

饿汉式 static final field

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

public class Singleton{
    //类加载时就初始化
    private static final Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

这种写法如果完美的话,就没必要在啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

静态内部类 static nested class

我比较倾向于使用静态内部类的方法,这种方法也是《Effective Java》上所推荐的。

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE; 
    }  
}

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

枚举 Enum

用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。

public enum EasySingleton{
    INSTANCE;
}

我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。

总结

一般来说,单例模式有五种写法:懒汉、饿汉、双重检验锁、静态内部类、枚举。上述所说都是线程安全的实现,文章开头给出的第一种方法不算正确的写法。

就我个人而言,一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。

41. 常见的设计模式

工厂模式:一个抽象接口的实现,多个抽象接口的实现类,spring的beanFactory就是工厂模式

单例模式:spring配置文件中配置的bean默认为单例模式

装饰者模式:对一个类进行装饰,增强其方法行为,如Java中的IO流就使用了装饰者模式

代理模式:比如动态代理

适配器模式:io流,通过继承实现将一个接口适配到另一个接口,InputStreamReader类继承Reader接口,但要创建它们必须在构造函数中传入一个InputStream的实例,InputStreamReader的作用也就是将InputStream适配到Reader

装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问

42. 单例模式的双检锁是什么

  关于单例模式,就不再详细叙述,想必大家都耳熟能详了,简单回顾下吧。以下是单例模式的一个例子:
public class DoubleCheckedLock {
    private static DoubleCheckedLock instance;  
    public static DoubleCheckedLock getInstance() { 
        if (instance == null) {  
        	instance=new DoubleCheckedLock();
        } 
        return instance; 
    }  
}
  上述的例子,如果是在并发的情况下,就会遇到严重的问题。比如线程A在判断instance为空时,进入new操作,new操作还未完成时,此时线程B也运行到判断instance是否为NULL,那么可能就会造成线程A和线程B都在new,那就违背了单例模式的原本含义了。那么既然需要保证只有一个实例,我们是否可以通过synchronized关键字来解决呢?
public class DoubleCheckedLock 
    private static DoubleCheckedLock instance;  
    public static synchronized DoubleCheckedLock getInstance() {  
        if (instance == null) { 
        	instance=new DoubleCheckedLock();
        }  
        return instance;  
    }  
}
不可否认,synchronized关键字是可以保证单例,但是程序的性能却不容乐观,原因在于getInstance()整个方法体都是同步的,这就限定了访问速度。其实我们需要的仅仅是在首次初始化对象的时候需要同步,对于之后的获取不需要同步锁。因此,可以做进一步的改进:
public class DoubleCheckedLock {
    private static DoubleCheckedLock instance;  
    public static DoubleCheckedLock getInstance() {  
        if (instance == null) {  //step1
        	synchronized (DoubleCheckedLock.class) { //step2
        		if(instance==null){ //step3
        			instance=new DoubleCheckedLock(); //step4
        		}
        	}
        }  
        return instance;  
    }  
}
这样我们将上锁的粒度降低到了仅仅是初始化实例的那部分,从而使代码即正确又保证了执行效率。这就是所谓的“双检锁”机制(顾名思义)。

 双检锁机制的出现确实是解决了多线程并行中不会出现重复new对象,而且也实现了懒加载,但是很可惜,这样的写法在很多平台和优化编译器上是错误的,原因在于:instance=new DoubleCheckedLock()这行代码在不同编译器上的行为是无法预知的。一个优化编译器可以合法地如下实现 instance=new DoubleCheckedLock():

\1. 给新的实体instance分配内存;

\2. 调用DoubleCheckedLock的构造函数来初始化instance。

现在想象一下有线程A和B在调用DoubleCheckedLock,线程A先进入,在执行到步骤4的时候被踢出了cpu。然后线程B进入,B看到的是instance已经不是null了(内存已经分配),于是它开始放心地使用instance,但这个是错误的,因为A还没有来得及完成instance的初始化,而线程B就返回了未被初始化的instance实例。

 当我们结合java虚拟机的类加载过程就会更好理解。对于JVM加载类过程,我还不是很熟悉,所以简要地介绍下:

jvm加载一个类大体分为三个步骤:
1)加载阶段:就是在硬盘上寻找java文件对应的class文件,并将class文件中的二进制数据加载到内存中,将其放在运行期数据区的方法区中去,然后在堆区创建一个java.lang.Class对象,用来封装在方法区内的数据结构;
2)连接阶段:这个阶段分为三个步骤,步骤一:验证,当然是验证这个class文件里面的二进制数据是否符合java规范;步骤二:准备,为该类的静态变量分配内存空间,并将变量赋一个默认值,比如int的默认值为0;步骤三:解析,这个阶段就不好解释了,将符号引用转化为直接引用,涉及到指针;
3)初始化阶段:当我们主动调用该类的时候,将该类的变量赋于正确的值(这里不要和第二阶段的准备混淆了),举个例子说明下两个区别,比如一个类里有private static int i = 5; 这个静态变量在"准备"阶段会被分配一个内存空间并且被赋予一个默认值0,当道到初始化阶段的时候会将这个变量赋予正确的值即5,了解了吧!

  因此,双检锁对于基础类型(比如int)适用。因为基础类型没有调用构造函数这一步。那么对于双检锁中因编译器的优化无法保证执行顺序的问题,具体地说是在C++下是精简指令集(RISC)机器的编译器会重新排列编译器生成的汇编语言指令,从而使代码能够最佳运用RISC处理器的平行特性,因此有可能破坏双检锁模式。对于此问题,查阅了不少解决方案,主要有以下几种:

1)使用memory barrier,,关于merrory barrier的介绍,可参阅博文Memory barrier

2)java中可考虑volatile关键字定义新的语意来解决这个问题,关于volatile关键字的使用,可见博文《volatile关键字》

1.面向对象的理解

      (1)**概述**:面向对象是相对于面向过程而言的,面向过程强调的是功能,面向对象强调的是将功能封装进对象,

	 强调具备功能的对象;

(2)**思想特点**:

	 A: 是符合人们思考习惯的一种思想;

	 B: 将复杂的事情简单化了;

	 C: 将程序员从执行者变成了指挥者;

	 比如我要达到某种结果,我就寻找能帮我达到该结果的功能的对象,如我要洗衣服我就买洗衣机,

	 至于怎么洗我不管。

(3)**特征**:

	封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式

	继承: 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义

	        这些属性和行为,只要继承那个类即可。

	多态: 一个对象在程序不同运行时刻代表的多种状态,父类或者接口的引用指向子类对象java的特性

封装:隐藏对象属性和实现细节;

继承:代码重用;

多态:运行时对象有多种状态;跨平台

43. 面向对象的特点

             各司其职: 对象应该保持其简单性

	弱耦合性: 对象和对象间的联系应该尽量弱化

	可重用性:

	可扩展性:

44. 多态的实现原理

靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。

45. 为什么类只能单继承,接口可以多继承

首先,类的多继承有哪些缺点那:

第一,如果一个类继承多个父类,如果父类中的方法名如果相同,那么就会产生歧义。

第二,如果父类中的方法同名,子类中没有覆盖,同样会产生上面的错误。

所以,java中就没有设计类的多继承。

但是接口就设计成多继承,是因为接口可以避免上述问题:

首先,接口中的只有抽象方法和静态常量。

对于一个类实现多个接口的情况和一个接口继承多个接口的情况,因为接口只有抽象方法,具体方法只能由实现接口的类实现(也是因为实现类一定会覆盖接口中的方法),在调用的时候始终只会调用实现类(也就是子类覆盖的方法)的方法(不存在歧义),因此不存在 多继承的第二个缺点;而又因为接口只有静态的常量,但是由于静态变量是在编译期决定调用关系的,即使存在一定的冲突也会在编译时提示出错;而引用静态变量一般直接使用类名或接口名,从而避免产生歧义,因此也不存在多继承的第一个缺点。

46. 面向对象的设计原则

1、 开闭原则Open Close Principle

**定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。**

2、里氏代换原则(Liskov Substitution Principle)

**定义1:如果对每一个类型为** **T1的对象** **o1,都有类型为** **T2** **的对象o2,使得以** **T1定义的所有程序** **P** **在所有的对象** **o1** **都代换成** **o2** **时,程序** **P** **的行为没有发生变化,那么类型** **T2** **是类型** **T1** **的子类型。**

**定义2:子类型必须能够替换掉它们的父类型。**

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:

  1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  2. 子类中可以增加自己特有的方法。
  3. 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

3、依赖倒转原则(Dependence Inversion Principle)

定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。即针对接 口编程,不要针对实现编程

      依赖倒置原则的中心思想是面向接口编程,传递依赖关系有三种方式,接口传递,构造方法传递和setter方法传递

在实际编程中,我们一般需要做到如下3点:

低层模块尽量都要有抽象类或接口,或者两者都有。

变量的声明类型尽量是抽象类或接口。

使用继承时遵循里氏替换原则。

总之,依赖倒置原则就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置

4、接口隔离原则(Interface Segregation Principle)

       接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

     其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。

采用接口隔离原则对接口进行约束时,要注意以下几点:

  1. 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。

  2. 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。

  3. 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

    运用接口隔离原则,一定要适度,接口设计的过大或过小都不好

  4. 组合/聚合复用原则

    就是说要尽量的使用合成和聚合,而不是继承关系达到复用的目的
    该原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分:新的对象通过向这些对象的委派达到复用已有功能的目的。
    其实这里最终要的地方就是区分“has-a”和“is-a”的区别。相对于合成和聚合,
    继承的缺点在于:父类的方法全部暴露给子类。父类如果发生变化,子类也得发生变化。聚合的复用的时候就对另外的类依赖的比较的少。。

合成/聚合复用
① 优点:
新对象存取成分对象的唯一方法是通过成分对象的接口;
这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的;

这种复用支持包装;
这种复用所需的依赖较少;
每一个新的类可以将焦点集中在一个任务上;
这种复用可以在运行时动态进行,新对象可以使用合成/聚合关系将新的责任委派到合适的对象。
② 缺点:
通过这种方式复用建造的系统会有较多的对象需要管理。

继承复用
① 优点:
新的实现较为容易,因为基类的大部分功能可以通过继承关系自动进入派生类;
修改或扩展继承而来的实现较为容易。
② 缺点:
继承复用破坏包装,因为继承将基类的实现细节暴露给派生类,这种复用也称为白箱复用;
如果基类的实现发生改变,那么派生类的实现也不得不发生改变;
从基类继承而来的实现是静态的,不可能在运行时发生改变,不够灵活。

6 、迪米特法则

迪米特法则其根本思想,是强调了类之间的松耦合,类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成影响,也就是说,信息的隐藏促进了软件的复用。

迪米特法则又叫最少知道原则,最早是在1987年由美国Northeastern University的Ian Holland提出。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。

一句话总结就是:一个对象应该对其他对象保持最少的了解。

7、单一职能原则

定义:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责,应该仅有一个引起它变化的原因

遵循单一职责原的优点有:

1.可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;

2.提高类的可读性,提高系统的可维护性;

3.变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。

需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都需要遵循这一重要原则

47. 对象封装的原则是什么?

封装的作用大概有两个:

1 对象的数据封装特性彻底消除了传统结构方法中数据与操作分离所带来的种种问题,提高了程序的可复用性可维护性,降低了程序员保持数据与操作内容的负担。

2 对象的数据封装特性还可以把对象的私有数据和公共数据分离开,保护了私有数据,减少了可能的模块间干扰,达到降低程序复杂性、提高可控性的目的。

通俗点说就是把一个对象下的属性概括起来,外部通过此对象来访问该对象下的属性,而开发人员可以通过控制属性的权限控制外部的访问权限,即可以控制哪些可以给外部访问哪些不行,可以起到很好的隐藏作用。

至于封装的原则主要是靠经验吧,只要尽力让封装的每个类里面的属性和方法独立,方便以后操作,减少耦合性就好了

48. 接口和抽象类的区别

抽象类可以有构造方法,接口中不能有构造方法

抽象类中可以有普通成员变量,接口中没有普通成员变量

抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的

抽象类中的抽象方法的访问类型可以是public,protected,但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型

抽象类中可以包含静态方法,接口中不能包含静态方法

抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型

一个类可以实现多个接口,但只能继承一个抽象类。

下面接着再说说两者在应用上的区别:

接口关注的是功能,抽象类关注的抽象现实中事物

49. **final、finally、finalize区别

final用于修饰类、成员变量和成员方法。

1、final修饰的类,不能被继承(String、StringBuilder、StringBuffer、Math,不可变类),其中所有的方法都不能被重写,所以不能同时用abstract和final修饰类(abstract修饰的类是抽象类,抽象类是用于被子类继承的,和final起相反的作用);

2、final修饰的方法不能被重写,但是子类可以用父类中final修饰的方法;

3、final修饰的成员变量是不可变的,如果成员变量是基本数据类型,初始化之后成员变量的值不能被改变,如果成员变量是引用类型,那么它只能指向初始化时指向的那个对象,不能再指向别的对象,但是对象当中的内容是允许改变的。

Final修饰的形参,不能在方法中对形参赋值

方法内声明的类或者方法内的匿名内部类,访问该方法内定义的变量,该变量必须要用final修饰。当内部类访问局部变量时,会扩大局部变量的作用域,如果局部变量不用 final 修饰,我们就可以在内部类中随意修改该局部变量值,而且是在该局部变量的作用域范围之外可以看到这些修改后的值,会出现安全问题。

finally通常和try catch搭配使用,保证不管有没有发生异常,资源都能够被释放(释放连接、关闭IO流)。

finalize是object类中的一个方法,子类可以重写finalize()方法实现对资源的回收。垃圾回收只负责回收内存,并不负责资源的回收,资源回收要由程序员完成,Java虚拟机在垃圾回收之前会先调用垃圾对象的finalize方法用于使对象释放资源(如关闭连接、关闭文件),之后才进行垃圾回收,这个方法一般不会显示的调用,在垃圾回收时垃圾回收器会主动调用。

50. 如何构建不可变的类结构?关键点在哪里

创建不可变类,要实现下面几个步骤:

  1. 将类声明为final,所以它不能被继承
  2. 将所有的成员声明为私有的,这样就不允许直接访问这些成员
  3. 对变量不要提供setter方法
  4. 将所有可变的成员声明为final,这样只能对它们赋值一次
  5. 通过构造器初始化所有成员,进行深拷贝(deep copy)
  6. 在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝

51. final关键字有哪些用法

一、final修饰类:

被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改,Java的一些核心的API都是final类,例如String、Integer、Math等

代码举例:

img

二、final修饰方法:

子类不可以重写父类中被final修饰的方法

代码举例:

img

三、final修饰实例变量(类的属性,定义在类内,但是在类内的方法之外)

final修饰实例变量时必须初始化,且不可再修改

final修饰实例变量时必须初始化代码举例:

img

final修饰实例变量时必须初始化,且不可再修改代码举例:

img

四、final修饰局部变量(方法体内的变量)

final修饰局部变量时只能初始化(赋值)一次,但也可以不初始化

代码举例:

img

五、final修饰方法参数

final修饰方法参数时,是在调用方法传递参数时候初始化的

代码举例:

img

52. throws, throw, try, catch, finally分别代表什么意义

throws是获取异常
throw是抛出异常
try是将会发生异常的语句括起来,从而进行异常的处理,
catch是如果有异常就会执行他里面的语句,
而finally不论是否有异常都会进行执行的语句。

throw和throws的详细区别如下:

**throw是语句抛出一个异常。**语法:throw (异常对象);
throw e;

throws是方法可能抛出异常的声明。(用在声明方法时,表示该方法可能要抛出异常)
语法:(修饰符)(方法名)([参数列表])[throws(异常类)]{…}
public void doA(int a) throws Exception1,Exception3{…}

  1. 区别

throws是用来声明一个方法可能抛出的所有异常信息,而throw则是指抛出的一个具体的异常类型。此外throws是将异常声明但是不处理,而是将异常往上传,谁调用我就交给谁处理。

  1. 分别介绍

throws:用于声明异常,例如,如果一个方法里面不想有任何的异常处理,则在没有任何代码进行异常处理的时候,必须对这个方法进行声明有可能产生的所有异常(其实就是,不想自己处理,那就交给别人吧,告诉别人我会出现什么异常,报自己的错,让别人处理去吧)。

格式是:方法名(参数)throws 异常类1,异常类2,…

53. Java中的Error和Exception

  • Error和Exception的联系

继承结构:Error和Exception都是继承于Throwable,RuntimeException继承自Exception。

Error和RuntimeException及其子类称为未检查异常(Unchecked exception),其它异常成为受检查异常(Checked Exception)。

  • Error和Exception的区别

Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。如java.lang.StackOverFlowError和Java.lang.OutOfMemoryError。对于这类错误,Java编译器不去检查他们。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和预防,遇到这样的错误,建议让程序终止。

Exception类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。

  • 运行时异常和受检查的异常

Exception又分为运行时异常(Runtime Exception)和受检查的异常(Checked Exception )。

RuntimeException:其特点是Java编译器不去检查它,也就是说,当程序中可能出现这类异常时,即使没有用try……catch捕获,也没有用throws抛出,还是会编译通过,如除数为零的ArithmeticException、错误的类型转换、数组越界访问和试图访问空指针等。处理RuntimeException的原则是:如果出现RuntimeException,那么一定是程序员的错误。

受检查的异常(IOException等):这类异常如果没有try……catch也没有throws抛出,编译是通不过的。这类异常一般是外部错误,例如文件找不到、试图从文件尾后读取数据等,这并不是程序本身的错误,而是在应用环境中出现的外部错误。

  • throw 和 throws两个关键字有什么不同

throw 是用来抛出任意异常的,你可以抛出任意 Throwable,包括自定义的异常类对象;throws总是出现在一个函数头中,用来标明该成员函数可能抛出的各种异常。如果方法抛出了异常,那么调用这个方法的时候就需要处理这个异常。

  • try-catch-finally-return执行顺序

1、不管是否有异常产生,finally块中代码都会执行;

2、当try和catch中有return语句时,finally块仍然会执行;

3、finally是在return后面的表达式运算后执行的,所以函数返回值是在finally执行前确定的。无论finally中的代码怎么样,返回的值都不会改变,仍然是之前return语句中保存的值;

4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。

54. 简述一个你最常见到的runtime exception(运行时异常)

ArithmeticException, 算术异常
ArrayStoreException, 将数组类型不兼容的值赋值给数组元素时抛出的异常
BufferOverflowException, 缓冲区溢出异常
BufferUnderflowException, 缓冲区下溢异常
CannotRedoException, 不能重复上一次操作异常
CannotUndoException, 不能撤销上一次操作异常
ClassCastException, 类型强制转换异常
ClassNotFoundException 类没找到时,抛出该异常
CMMException, CMM异常
ConcurrentModificationException, 对Vector、ArrayList在迭代的时候如果同时对其进行修改就会抛出异常
org.springframework.jdbc.CannotGetJdbcConnectionException 服务器端数据库连接不上时,抛出该异常
CannotGetJdbcConnectionException 网络没有连接或网络中断
DOMException, DOM异常
EOFException, 文件已结束异常
EmptyStackException, 空栈异常
FileNotFoundException, 文件未找到异常
IllegalArgumentException, 传递非法参数异常
IllegalMonitorStateException,
IllegalAccessException, 访问某类被拒绝时抛出的异常
IllegalPathStateException, 非法的路径声明异常
IllegalStateException, 非法声明异常
ImagingOpException, 成像操作异常
IndexOutOfBoundsException, 下标越界异常
IOException, 输入输出异常
NegativeArraySizeException, 数组负下标异常
NoSuchMethodException 在类中无法找到某一特定方法时,抛出该异常
NoSuchElementException, 方法未找到异常
NoSuchFieldException 类不包含指定名称的字段时产生的信号(bean中不存在这个属性)
NumberFormatException, 字符串转换为数字异常
NullPointerException, 空指针异常
ProfileDataException, 没有日志文件异常
ProviderException, 供应者异常
RasterFormatException, 平面格式异常
SecurityException, 违背安全原则异常
SQLException, 操作数据库异常
SystemException, 系统异常
UndeclaredThrowableException,
UnmodifiableSetException,
UnsupportedOperationException, 不支持的操作异常

55. 空指针异常总结

1:NullPointerException由RuntimeException派生出来,是一个运行级别的异常。意思是说可能会在运行的时候才会被抛出,而且需要看这样的运行级别异常是否会导致你的业务逻辑中断。

2:空指针异常发生在对象为空,但是引用这个对象的方法。例如: String s = null; //对象s为空(null) int length = s.length();//发生空指针异常

3:一个变量是null,及只有其名,没有实值内容,也没分配内存,当你要去取他的长度,对他进行操作就会出现NullPointException,所以生命一个变量时最好给它分配好内存空间,给予赋值。

4:比如变量为空,而你没有去判断,就直接使用,就会出现NullPointException。写程序时严谨些,尽量避免了,例如在拿该变量与一个值比较时,要么先做好该异常的处理如: if (str == null) { System.out.println(“字符为空!”); } 当然也可以将这个值写在前面进行比较的,例如,判断一个String的实例s是否等于“a”,不要写成s.equals(“a”),这样写容易抛出NullPointerException,而写成"a".equals(s)就可以避免这个问题。不过对变量先进行判空后再进行操作比较好

5:尽量避免返回null,方法的返回值不要定义成为一般的类型,而是用数组。这样如果想要返回null的时候,就返回一个没有元素的数组。就能避免许多不必要的NullPointerException,使用NullObject返回代替返回null确是一种不错的选择。

6:NullPointerException这个东西换一个角度来看,没准是好处也不一定。可以说,NullPointerException本身也是JAVA安全机制的一部分。有UNIX写C和C++的经验的可能都知道,空指针会导致什么问题:经常会导致程序的崩溃。 ? 而JAVA在这点进行了改善,JAVA为了保证程序的强壮,总是会对对象的引用进行检查。所以不再出险C/C++中的空指针错误,而仅仅是一个运行级别的异常-“NullPointerException”。从这点上说,算是JAVA的一个好处吧。 Josha Bloch倒是在《Effective Java》中说过返回数组的函数,如果没有返回值,优先返回零长度数组而不是返回null。 ? 不过使用NullObject返回代替返回null确是一种不错的选择。返回数组的方法同样可以返回null,因为数组在JAVA中已经发展为完备的对象了。如果是这样,INVOKER也是不可避免地检查NullPointerException。 初值不是你想决定是什么就是什么的。在很多情况下,你甚至无法断定对象的初值是什么才合适。所以这样的习惯并不见得就是很好的习惯。比如说你认为: String str = “”; 这样比较合理,但是为什么不是String str = “A”; 呢?在某些场合并不见得""就是合理的初值。关键还是在建立publish方法的契约之上。如果你使用第三方的方法,你需要阅读其JAVADOC,知道其是否会返回null对象?是否会抛出checkedException,是否会抛出运行级别异常。如果是你自己publish方法,那么你需要在你的JAVADOC中说明你的方法的契约:满足什么条件才能调用此方法,调用之后会产生什么返回?是否会返回/何时返回null?是否抛出异常。在实现publish方法的时候,对于入口参数的检查也是非常关键的,因为调用者的行为是你无法期望的。 其实异常的处理是一个很有意思的话题,不仅仅只是NullPointerException。比如在DBC中有这么一个例子:你需要打开一个文件读,可能是C:\Data.txt,文件却没有找到,叫不叫异常?你如果需要打开另外一个文件,比如是C:\boot.ini,文件也没有找到,叫不叫异常?第一种情况不叫“异常”,因为C:\Data.txt没有找到应该是你能预计到的情况,那个文件可能存在,也可能不存在,这是需要你自己处理的。而第二种情况确叫做异常,因为正常情况下,C:\boot.ini应该被期望存在的,如果运行时丢失了这个文件,就是运行级别异常。在JDK中也有相应的例子,比如FileInputStream, BufferReader, StringTokenizer处理到达尾部的情况就是不一样的。 <<4.为什么说ResultSet作为处理结果不恰当呢?>> 把不相关的东西耦合在了一起。把JDBC query的结果集逻辑和自己需要的数据聚集的逻辑耦合在了一起。这样连抽象都无法做。现在俺们是从数据库中随机取结果,所以你使用了ResultSet,假设需求变化了,需要从文本文件源中取结果了,你的ResultSet接口成了什么?所以说更好的做法是抽象出数据获取的接口,而针对接口不同实现即可。 ? “单一职责”这点很关键,不需要耦合在一起的东西就不要耦合在一起。

7:我在写程序时,String uri=getSavePath()+getUploadFileName(); System.out.println(uri+“123345”);//这里可以输出结果的 String linkname=getUploadFileName(); String update=“update UserTable set uri=’?’,linkname=’?’,linkid=’?'where username=”+"’"+username+"’"; DataBaseOperate dbo=new DataBaseOperate(); PreparedStatement ps=dbo.getConnection1().prepareStatement(update); ps.setString(1, uri); //这一行居然报出空指针异常呢? ps.setString(2, linkname); ps.setInt(3, i++); ps.executeUpdate(); 原来是加?号是为了可以自动给变量给值‘?’ 变成了字符常量了 …

56. 你曾经自定义实现过异常吗?怎么写的?

很显然,我们绝大多数都写过自定义或者业务异常,像AccountNotFoundException。在面试过程中询问这个Java异常问题的主要原因是去发现你如何使用这个特性的。这可以更准确和精致的去处理异常,当然这也跟你选择checked 还是unchecked exception息息相关。通过为每一个特定的情况创建一个特定的异常,你就为调用者更好的处理异常提供了更好的选择。相比通用异常(general exception),我更倾向更为精确的异常。大量的创建自定义异常会增加项目class的个数,因此,在自定义异常和通用异常之间维持一个平衡是成功的关键。

57. finally代码块的执行情况

一、try里有return,finally怎么执行

finally块里的代码是在return之前执行的。

在异常处理中,无论是执行try还是catch,finally{}中的代码都会执行(除非特殊情况)。由于程序执行return就意味着结束对当前函数的调用并跳出这个函数体,因此任何语句要执行都只能在return前执行。

public class Test {

public static int testFinally() {

    try {

        return 1;

    } catch (Exception ex) {

        return 2;

    } finally {

        System.out.println("execute finally");

    }

}

public static void main(String[] args) {

    int result = testFinally();

    System.out.println(result);

}

}

运行结果

execute finally1

· 此外,如果try-catch-finally中都有return,那么finally块中的return将会覆盖别处的return语句,最终返回到调用者那里的是finally中return的值。

public class Test {

public static int testFinally() {

    try {

        return 1;

    } catch (Exception ex) {

        return 2;

    } finally {

        System.out.println("execute finally");

        return 3;

    }

}

public static void main(String[] args) {

    int result = testFinally();

    System.out.println(result);

}

}

运行结果:

execute finally3

return语句并不一定都是函数的出口,执行return时,只是把return后面的值复制了一份到返回值变量里去了。

· 此外,在try/catch中有return时,在finally块中改变基本类型的数据对返回值没有任何影响;而在finally中改变引用类型的数据会对返回结果有影响。

/**

* try/catch中有return,在finally{}中改变基本数据类型、引用类型对运行结果的影响

*/public class Test {

public static int testFinally1() {

    int result1 = 1;

    try {

        return result1;

    } catch (Exception ex) {

        result1 = 2;

        return result1;

    } finally {

        result1 = 3;

        System.out.println("execute testFinally1");

    }

}

public static StringBuffer testFinally2() {

    StringBuffer result2 = new StringBuffer("hello");

    try {

        return result2;

    } catch (Exception ex) {

        return null;

    } finally {

        result2.append("world");

        System.out.println("execute testFinally2");

    }

}

public static void main(String[] args) {

    int test1 = testFinally1();

    System.out.println(test1);

    StringBuffer test2 = testFinally2();

    System.out.println(test2);

}

}

运行结果:

execute testFinally1

1

execute testFinally2

helloworld

程序在执行到return时会先将返回值存储在一个指定位置,其次去执行finally块,最会再return。

在finally块中改变基本类型的数据result1/引用类型数据result2的值,与java的值传递和引用传递相关。值传递中,形参和实参有着不同的存储单元,对形参的改变不会影响实参的值;引用传递中,传递的是对象的地址,形参和实参的对象指向同一块存储单元对形参的改变就会影响实参的值。

二、如果执行finally代码块之前方法返回了结果,或者JVM退出了,finally块中的代码还会执行吗//finally不会执行的情况

java程序中的finally块并不一定会被执行。
至少有两种情况finally语句是不会执行的。

(1)try语句没有被执行到。
即没有进入try代码块,对应的finally自然不会执行。

比如,在try语句之前return就返回了,这样finally不会执行;
或者在程序进入java之前就出现异常,会直接结束,也不会执行finally块。

(2)在try/catch块中有System.exit(0)来退出JVM。

System.exit(0)是终止JVM的,会强制退出程序,finally{}中的代码就不会被执行

58 static 关键字是什么意思?Java中是否可以覆盖(override)一个private或者是static的方法

Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。

java中也不可以覆盖private的方法,因为private修饰的变量和方法只能在当前类中使用,如果是其他的类继承当前类是不能访问到private变量或方法的,当然也不能覆盖。

59 main() 方法为什么必须是静态的?能不能声明 main() 方法为非静态

所有static成员都是在程序装载时初始化的,被分配在一块静态存储区域。

这个区域的成员一旦被分配,就不再改变地址啦。直到程序结束才释放。
main()就存储在这里。
尽管包含main()的类还没有被实例化,但是main()方法已经可以使用啦。
而且JVM将会自动调用这个方法。通过main()的调用,再实例化其他的对象,
也包括自己所在的类。

我们知道,在C/C++当中,这个main方法并不是属于某一个类的,它是一个全局的方法,所以当我们执行的时候,c++编译器很容易的就能找到这个main方法,然而当我们执行一个java程序的时候,因为java都是以类作为程序的组织单元,当我们要执行的时候,我们并不知道这个main方法会放到哪个类当中,也不知道是否是要产生类的一个对象,为了解决程序的运行问题,我们将这个main方法定义为static,这样的话,当我们在执行一个java代码的时候,我们在命令提示符中写:java Point(Point为一个类),解释器就会在Point这个类当中,去调用这个静态的main方法,而不需要产生Point这个类的对象,当我们加载Point这个类的时候,那么main方法也被加载了,作为我们java程序的一个入口。

60. 是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用

不可以,静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在调用静态方法时可能对象并没有被初始化

61 静态类型有什么特点

1、随着类的加载而加载

也就是说:静态会随着类的消失而消失,说明他的生命周期最长

2、优先于对象存在

3、被所有对象所共享

4、可以直接被类名调用

62. 静态变量在什么时候加载?编译期还是运行期?静态代码块加载的时机呢

当类加载器将类加载到JVM中的时候就会创建静态变量,这跟对象是否创建无关。静态变量加载的时候就会分配内存空间。静态代码块的代码只会在类第一次初始化的时候执行一次。一个类可以有多个静态代码块,它并不是类的成员,也没有返回值,并且不能直接调用。静态代码块不能包含this或者super,它们通常被用初始化静态变量。

63. 什么是 Java 的反射机制

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;

对于任意一个对象,都能够调用它的任意一个方法和属性;

这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

要想解剖一个类,必须先要获取到该类的字节码文件对象。

而解剖使用的就是Class类中的方法,所以先要获取到每一个字节码文件对应的Class类型的对象。

64. 获得类对象

第一种方式:通过类本身来获得对象

Java代码  
Class classname = 
this
.getClass();  

第二种方式:通过子类的实例获取父类对象

Java代码  
ClassName cn = 
new
 ClassName();  
UserClass = cn.getClass();  
Class SubUserClass = UserClass.getSuperclass();   

第三种方式:通过类名加.class获取对象

Java代码  
Class ForClass = **.**.ClassName.
class
;(类在包中的路径加.
class
)  

第四种方式:通过类名的字符串获取对象

Java代码  
Class ForName = Class.forName(
"**.**.ClassName"
);

**** 65 java反射机制提供了什么功能?

在运行时能够判断任意一个对象所属的类
在运行时判断任意一个类所具有的成员变量和方法
在运行时调用任一对象的方法、设置属性值
在运行时创建类对象

66. 哪里用到反射机制?

jdbc中有一行代码:Class.forName(‘com.mysql.jdbc.Driver.class’).newInstance();那个时候只知道生成驱动对象实例,后来才知道,这就是反射,现在很多框架都用到反射机制,hibernate,struts都是用反射机制实现的。

以直接使

67. 反射机制的优缺点?

静态编译:在编译时确定类型,绑定对象,即通过
优点:
动态编译:运行时确定类型,绑定对象。动态编译最大限度的发挥了java的灵活性,体现了多态的应用,有利于降低类之间的耦合性。
缺点:
它的缺点是对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于只直接执行相同的操作。

68. 对象引用的分类

1, 强引用

只要引用存在,垃圾回收器永远不会回收

Objectobj = new Object();

// 可直接通过物镜取得对应的对象如obj.equels(NEWOBJECT());

而这样 obj对象对后面newObject的一个强引用,只有当obj这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。

2、 软引用

非必须引用,内存溢出之前进行回收,可以通过以下代码实现

Objectobj = new Object();

SoftReferencesf = new SoftReference(obj);

obj =null;

sf.get();//有时候会返回null

这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;
软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。

3、 弱引用

第二次垃圾回收时回收,可以通过如下代码实现

Objectobj = new Object();

WeakReferencewf = new WeakReference(obj);

obj =null;

wf.get();//有时候会返回null

wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。

弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器

4、 虚引用(幽灵/幻影引用)
       垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现

Objectobj = new Object();

PhantomReferencepf = new PhantomReference(obj);

obj=null;

pf.get();//永远返回null

pf.isEnQueued();//返回从内存中已经删除

虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。

虚引用主要用于检测对象是否已经从内存中删除。

69. 泛型的作用

java 泛型是java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。

  Java 语言中引入泛型是一个较大的功能增强。不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了。

这带来了很多好处:
1,类型安全。 泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。

2,消除强制类型转换。 泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。

3,潜在的性能收益。 泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。

Java语言引入泛型的好处是安全简单。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

泛型在使用中还有一些规则和限制:
1、泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。
2、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
3、泛型的类型参数可以有多个。
4、泛型的参数类型可以使用extends语句,例如<T extends superclass>。习惯上成为“有界类型”。
5、泛型的参数类型还可以是通配符类型。例如Class<?> classType = Class.forName(Java.lang.String);

70. 泛型的特点

        java中的泛型只是在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也成为裸类型),并且在相应的地方插入了强制转换代码,因此对于运行期的Java语言来讲,所有的泛型容器都是一样的,泛型技术实际上是Java的一颗语法糖,Java语言的泛型实际方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。

71. 克隆

https://blog.csdn.net/w410589502/article/details/54985987

1. 为什么要克隆

在java中,复制一个变量很容易

int a = 5; int b = a;

不仅仅是int类型,其它七种原始数据类型(boolean,char,byte,short,float,double.long)同样适用于该类情况。
但是如果我们需要复制一个对象,使用变量的”=”符号复制就不正确了,除了在函数传值的时候是”引用传递”,在任何用”=”向对象变量赋值的时候都是”引用传递”。

克隆的对象可能包含一些已经修改过的属性,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠clone方法了。
所以我们需要能够实现对于输入的实参进行了一份拷贝,若方法参数为基本类型,则在栈内存中开辟新的空间,所有的方法体内部的操作都是针对这个拷贝的操作,并不会影响原来输入实参的值 。若方法参数为引用类型,该拷贝与输入实参指向了同一个对象,方法体内部对于对象的操作,都是针对的同一个对象。

2. 如何实现克隆

两种不同的克隆方法,浅克隆(ShallowClone)和深克隆(DeepClone)。
在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制。
有两种方式:
1). 实现Cloneable接口并重写Object类中的clone()方法;
2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆;

3. 深拷贝和浅拷贝区别 (深克隆和浅克隆)

浅克隆

在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。
img
在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。

深克隆

在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。
简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
img
在Java语言中,如果需要实现深克隆,可以通过覆盖Object类的clone()方法实现,也可以通过序列化(Serialization)等方式来实现。
如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。
序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值