# 一、JAVA中的几种基本数据类型

Java语言中一共提供了8种原始的数据类型(byte,short,int,long,float,double,char,boolean),这些数据类型不是对象,而是 Java语言中不同于类的特殊类型,这些基本类型的数据变量在声明之后就会立刻在栈上被分配内存空间。除了这8种基本的数据类型外,其他类型都是引用类型(例如类、接口、数组等),引用类型类似于C++中的引用或指针的概念,它以特殊的方式指向对象实体,此类变量在声明时不会被分配内存空间,只是存储了一个内存地址而已。

数据类型 字节长度 范围 默认值 包装类
int 4 (-2^31~2^31-1) 0 Integer
short 2 [-32768,32767] 0 Short
byte 1 [-128,127] 0 Byte
long 8 (-2^63~2^63-1) 0L或0l Long
double 8 64位IEEE754双精度范围 0.0 Double
float 4 32位IEEE754单精度范围 0.0F或0.0f Float
char 2 Unicode [0,65535] u0000 Character
boolean 1 true和false flase Boolean

# 二、String 类能被继承吗

不可以,因为 String类有 final修饰符,而 final修饰的类是不能被继承的,实现细节也不允许改变。

# 三、String,StringBuffer,StringBuilder的区别

【1】String是不可变类,String对象一旦被创建,其值就不能改变,而 StringBuffer是可变类,当对象被创建后仍然可以对其值进行修改。由于 String是不可变类,适合在共享场合中使用,而当一个字符串经常被修改时,最好使用 StringBuffer来实现。如果用 String保存一个经常修改的字符串时,字符串被修改时会比 StringBuffer多很多附加的操作,同时生成很多无用的对象,这些无用的对象会被垃圾回收器回收,从而影响程序的性能。在规模小的项目里面这个影响很小,但是在一个规模大的项目里面,这会对程序的运行效率带来很大的影响。

【2】String 与 StringBuffer实例化时存在区别:String 可以通过构造函数的方式(String s = new String("hello"))和直接赋值(String s="world")两种方式。而 StringBuffer只能使用构造函数进行赋值(StringBuffer sb = new StringBuffer("hello"))。

【3】String 字符串修改实现的原理:当 String修改字符串时,先创建一个 StringBuffer,其次调用 append()方法,最后调用 toString()方法把结果返回。实例如下(下述过程比使用 StringBuffer多了一些附加操作,同时也生成了一些临时的对象,从而导致程序执行效率下降):

String s = "HELLO";
s+="WORLD";
//以上代码 实现底层 如下
StringBuffer sb = new StringBuffer(s);
sb.append("WORLD");
s=sb.toString();
1
2
3
4
5
6

【4】StringBuilder:可以被修改的字符串,他与 StringBuffer类似,都是字符缓冲区,但 StringBuild不是线程安全的,如果只是单线程访问时可以使用 StringBuilder,当有多个线程访问时,最好使用线程安全的 StringBuffer。因为 StringBuffer必要时会对这些方法进行同步,所以任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。

【5】在执行效率方面:StringBuilder 最高,StringBuffer 次之,String 最低,鉴于以上情况,一般使用数据量较小的情况下,优先使用 String;如果单线程下使用大量数据,应优先使用 StringBuilder类;如果是在多线程下操作大量数据,应优先考虑 StringBuffer类。

# 四、List 和 Set有什么区别

ArrayList、Vecotr、LinkedList 类均为 java.util 包中,均为可伸缩数据,既可以动态改变长度的数组。都是 list 接口的实现类;

【1】ArrayList 和 Vector都是基于存储的 Object[] array 来实现的,它们会在内存中开辟一块连续的空间来存储(默认是10数组大小的内存),由于数据存储是连续的,因此,它们支持用序号(下标)来访问元素,同时索引数据的速度比较快。但是在插入/删除元素时需要移动容器中的元素,所以对数据的插入/删除操作执行比较慢。ArrayList 和 Vector都是一个初始化的容量的大小,当存储的元素超过分配内存大小时就需要动态地扩展它们的存储空间(会重新创建一个新的数组,将旧的数据复制过去)。为了提高效率,每次扩充容量时,不是简单的扩充一个存储单位,而是一次增加多个存储单元,Vector 默认扩充为原来的2倍(每次扩充的大小是可以设置的),而 ArrayList 默认扩充1.5倍(没有提供方法来设置空间扩充大小)。

【2】ArrayList 与 Vector最大的区别就是 synchronization(同步)的使用,没有一个 ArrayList的方法是同步,而 Vector的绝大数方法(add、insert、remove、set、equals、hashcode等)都是直接或者间接同步的,所以 Vector是线程安全的,ArrayList 不是线程安全的。正是由于 Vector是线程安全的,所以性能上也略逊于 ArrayList。

【3】LinkedList 是采用带有头节点和尾节点的双向链表来实现的,对数据的索引需要从列表头开始遍历,因此用于随机访问效率比较低,但是插入元素时不需要对数据进行移动,因此插入效率高。同时 LinkedList是非线程安全的容器。提供了两个插入方法:头插法(LinkedFirst)和尾插法(LinkedLast)。

JAVA基础

【4】那么,在实际使用时,当对数据主要操作为索引或只是集合的未端增加、删除元素时,使用 ArralyList或 Vector效率比较高;当对数据的操作主要是指位置的插入或者删除操作时,使用 LinkedList效率比较高;当在线程中使用容器时(既多线程同时访问该容器),选用 Vector较为安全。

set(元素无放入顺序,元素不可重复,重复元素会覆盖掉),只能用迭代获取元素。不能直接遍历集合获取。set接口下的常用子类说明;

【5】HashSet是 Set接口的典型实现,HashSet使用 Hash算法来存储集合中的元素,因此具有良好的存取和查找性能。当向 HashSet集合中存入一个元素时,HashSet会调用该对象的 hashCode()方法来得到该对象的 hashCode值,然后根据该 HashCode值决定该对象在 HashSet中的存储位置。值得主要的是,HashSet集合判断两个元素相等的标准是两个对象通过 equals()方法比较相等,并且两个对象的 hashCode()方法的返回值相等。

【6】LinkedHashSet 继承自 HashSet 与 LinkedHashMap 相似,是对 LinkedHashMap 的封装。根据插入的顺序进行排列。

【7】TreeSetTreeSet 是SortedSet接口的实现类,TreeSet 根据 key进行排序,确保集合元素处于有序状态。 线程安全应该使用的集合链接

# 五、讲讲类的实例化顺序

类加载器实例化时进行的操作步骤(加载【查找并加载类的二进制数据】—>链接【1、验证:确保被加载的类的正确性。2、准备:为类的静态变量分配内存,并将其初始化为默认值】—>解析【把类中的符号引用转换为直接引用】 —>初始化【为类的静态变量赋予正确的初始值】)。所有的 Java虚拟机实例必须在每个类或接口被 Java程序“首次主动使用”时才初始化它们。

执行顺序: 父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量(父类实例成员变量)、父类构造函数、子类非静态变量(子类实例成员变量)、子类构造函数。

# 六、你知道的容器(Map)类

Java为数据结构中的映射定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap 和 TreeMap,类继承关系如下图所示:

JAVA基础

【1】HashMap: 最常用的 Map,它根据键的 HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。它是线程不安全的Map,方法上都没有 synchronize关键字修饰。HashMap允许空(null)键值,但最多只允许一条记录的键为null,允许多条记录的值为null。

【2】HashTable: Hashtable是遗留类,很多映射的常用功能与 HashMap类似,不同的是它承自 Dictionary类,是线程安全的 Map实现类,它实现线程安全的方法是在各个方法上添加了 synchronize关键字,但是现在已经不再推荐使用 HashTable了,因为现在有了 ConcurrentHashMap这个专门用于多线程(并发)场景下的 Map实现类,其大大优化了多线程下的性能。

TIP

如果你经常参加面试,一定会被问到这个 Map实现类,这个 Map实现类是在 jdk1.5中加入的,其在 jdk1.6/1.7中的主要实现原理是 segment段锁,而每个Segment 都继承了 ReentrantLock 类,也就是说每个 Segment类本身就是一个锁。使用 put 方法的时候,是对我们的 key进行 hash拿到一个整型,然后将整型对16取模,拿到对应的Segment,之后调用 Segment的 put方法,然后上锁,这里 lock()的时候其实是 this.lock(),也就是说,每个 Segment的锁是分开的。它不再使用和 HashTable一样的 synchronize一样的关键字对整个方法进行加锁,而是转而利用 segment 段落锁来对其进行加锁,以保证 Map的多线程安全。其实可以理解为,一个 ConcurrentHashMap 是由多个 HashTable组成,所以它允许获取到不同段锁的线程同时持有该资源,也就是说 segment有多少个,理论上就可以同时有多少个线程来持有它这个资源。其默认的 segment是一个数组,默认长度为16。也就是说理论上可以提高16倍的性能。在 Java 的 jdk1.8中则对 ConcurrentHashMap又再次进行了大的修改,取消了 segment段锁字段,采用了CAS+Synchronize技术来保障线程安全。具体7中有介绍。

【3】TreeMap: TreeMap实现 SortedMap接口,是一个很常用的 Map实现类,因为他具有一个很大的特点就是会对 Key进行排序,使用了 TreeMap存储键值对,再使用 Iterator进行输出时,会发现其默认采用 key由小到大的顺序输出键值对,如果想要按照其他的方式来排序,需要重写也就是 override 它的 compartor 接口。TreeMap 底层的存储结构也是一颗红黑树。红黑树查找效率高,只有O(logn)。它是一种自平衡的二叉查找树。在每次插入和删除节点时,都可以自动调节树结构,以保证树的高度是 logn。

public class Compare {
     public static void main(String[] args) {
     TreeMap<String,Integer> map = new TreeMap<String,Integer>(new xbComparator());
         map.put("key_1", 1);
         map.put("key_2", 2);
         map.put("key_3", 3);
         Set<String> keys = map.keySet();
         Iterator<String> iter = keys.iterator();
         while(iter.hasNext()){
             String key = iter.next();
             System.out.println(" "+key+":"+map.get(key));
         }
     }
 }
     //重写排序方法
     class xbComparator implements Comparator{
         public int compare(Object o1,Object o2){
         String i1=(String)o1;
         String i2=(String)o2;
         return -i1.compareTo(i2);
     }
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

【4】LinkedHashMap: 它的特点主要在于 Linked,带有这个字眼的就表示底层用的是链表来进行的存储。相对于其他的无序的 Map实现类,还有像 TreeMap这样的排序类,LinkedHashMap最大的特点在于有序,但是它的有序主要体现在先进先出 FIFIO上。没错,LinkedHashMap主要依靠双向链表和 hash表来实现的,在用 Iterator遍历 LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

【5】WeakHashMap: 与 HashMap类似,二者不同之处在于 WeakHashMap中的 key不再被外部引用,它就可以被垃圾回收器回收。而 HashMap中 key采用的是“强引用的方式”,当 HashMap中的 key没有被外部引用时,只有在这个 key从 HashMap中删除后,才可以被垃圾回收器回收。

# 七、Java8 的 ConcurrentHashMap 为什么放弃了分段锁

ConcurrentHashMap 分段锁中存在一个分段锁个数的问题,既 Segment[] 的数组长度。当长度设置小了,数据量大时会出现额外的竞争,当线程试图写入当前锁定的段时会导致阻塞。相反,如果高估了并发级别,当遇到过大的膨胀(大量的并发),由于段产生的不必要数量,这种膨胀会导致性能的下降。因为高速缓存未命中。而 Java8中仅仅是为了兼容旧版本而保留。唯一的作用就是保证构造 Map时初始容量不小于 concurrencyLevel。

在 Java的 jdk1.8中则对 ConcurrentHashMap采用 CAS+Synchronize(取代 Segment+ReentrantLock)技术来保障线程安全,底层采用数组+链表+红黑树[当链表长度为8时,使用红黑树]的存储结构,也就是和 HashMap一样。这里注意 Node其实就是保存一个键值对的最基本对象。其中 value和 next都是使用的 volatile关键字进行了修饰,以确保线程安全。

TIP

volatile 修改变量后,此变量就具有可见性,一旦该变量修改,其他线程立马就会知道,立马放弃自己在自己工作内存中持有的该变量值。转而重主内存中获取该变量最新的值。

在插入元素时,会首先进行 CAS判定,如果 OK就是插入其中,并将 size+1,但是如果失败了,就会通过自旋锁自旋后再次尝试插入,直到成功。所谓 CAS也就是 Compare And Swap,既在更改前先对内存中的变量值和你指定的那个变量值进行比较,如果相同就说明再此期间没有被修改,而如果不一样了,则就要停止修改,否则就会影响到其他人的修改,将其覆盖掉。举例:内存值a,旧值b,和要修改后的值c,如果这里a=b,那么就可以进行更改,就可以将内存值a=c。否则就要终止该更新操作。如果链表中存储的 Entry超过了8个则就会自动转换链表为红黑树,提高查询效率。源码如下:

/*
 * 当添加一对键值对的时候,首先会去判断保存这些键值对的数组是不是初始化了,
 * 如果没有的话就初始化数组
 *  然后通过计算hash值来确定放在数组的哪个位置
 * 如果这个位置为空则直接添加,如果不为空的话,则取出这个节点来
 * 如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制
 * 最后一种情况就是,如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作
 *    然后判断当前取出的节点位置存放的是链表还是树
 *    如果是链表的话,则遍历整个链表,直到取出来的节点的key来个要放的key进行比较,如果key相等,并且key的hash值也相等的话,
 *          则说明是同一个key,则覆盖掉value,否则的话则添加到链表的末尾
 *    如果是树的话,则调用putTreeVal方法把这个元素添加到树中去
 *  最后在添加完成之后,会判断在该节点处共有多少个节点(注意是添加前的个数),如果达到8个以上了的话,
 *  则调用treeifyBin方法来尝试将处的链表转为树,或者扩容数组
 */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();//K,V都不能为空,否则的话抛出异常
    int hash = spread(key.hashCode());    //取得key的hash值
    int binCount = 0;    //用来计算在这个节点总共有多少个元素,用来控制扩容或者转移为树
    for (Node<K,V>[] tab = table;;) {    //
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();    //第一次put的时候table没有初始化,则初始化table
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {    //通过哈希计算出一个表中的位置因为n是数组的长度,所以(n-1)&hash肯定不会出现数组越界
            if (casTabAt(tab, i, null,        //如果这个位置没有元素的话,则通过cas的方式尝试添加,注意这个时候是没有加锁的
                         new Node<K,V>(hash, key, value, null)))        //创建一个Node添加到数组中区,null表示的是下一个节点为空
                break;                   // no lock when adding to empty bin
        }
        /*
         * 如果检测到某个节点的hash值是MOVED,则表示正在进行数组扩张的数据复制阶段,
         * 则当前线程也会参与去复制,通过允许多线程复制的功能,一次来减少数组的复制所带来的性能损失
         */
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            /*
             * 如果在这个位置有元素的话,就采用synchronized的方式加锁,
             *     如果是链表的话(hash大于0),就对这个链表的所有元素进行遍历,
             *         如果找到了key和key的hash值都一样的节点,则把它的值替换到
             *         如果没找到的话,则添加在链表的最后面
             *  否则,是树的话,则调用putTreeVal方法添加到树中去
             *
             *  在添加完之后,会对该节点上关联的的数目进行判断,
             *  如果在8个以上的话,则会调用treeifyBin方法,来尝试转化为树,或者是扩容
             */
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {        //再次取出要存储的位置的元素,跟前面取出来的比较
                    if (fh >= 0) {                //取出来的元素的hash值大于0,当转换为树之后,hash值为-2
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {    //遍历这个链表
                            K ek;
                            if (e.hash == hash &&        //要存的元素的hash,key跟要存储的位置的节点的相同的时候,替换掉该节点的value即可
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)        //当使用putIfAbsent的时候,只有在这个key没有设置值得时候才设置
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {    //如果不是同样的hash,同样的key的时候,则判断该节点的下一个节点是否为空,
                                pred.next = new Node<K,V>(hash, key,        //为空的话把这个要加入的节点设置为当前节点的下一个节点
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {    //表示已经转化成红黑树类型了
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,    //调用putTreeVal方法,将该元素添加到树中去
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)    //当在同一个节点的数目达到8个的时候,则扩张数组或将给节点的数据转为tree
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);    //计数
    return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

Synchronized 是靠对象头和对象的 monitor来保证上锁的,也就是对象头里的重量级锁标志指向了monitor,而 monitor呢,内部则保存了一个当前线程,也就是抢到了锁的线程。

那么这里的这个f是什么呢?数组下标的某一个节点对象,下标是通过插入Node节点的hash()值得到的。

如果使用 ReentrantLock其实也可以将锁细化成这样的,只要让 Node类继承 ReentrantLock就行了,这样的话调用 f.lock()就能做到和 Synchronized(f)同样的效果,但为什么不这样做呢?

因为锁已经被细化到这种程度了,那么出现并发争抢的可能性还高吗?还有就是,哪怕出现争抢,只要线程可以在30到50次自旋里拿到锁,那么 Synchronized就不会升级为重量级锁,而等待的线程也就不用被挂起,我们也就少了挂起和唤醒这个上下文切换的过程开销。

但如果是 ReentrantLock呢?它则只有在线程没有抢到锁,然后新建 Node节点后再尝试一次而已,不会自旋,而是直接被挂起,这样一来,我们就很容易会多出线程上下文开销的代价。当然,你也可以使用 tryLock(),但是这样又出现了一个问题,你怎么知道 tryLock的时间呢?在时间范围里还好,假如超过了呢?

所以,在锁被细化到如此程度上,使用 Synchronized是最好的选择了。这里再补充一句,Synchronized 和 ReentrantLock他们的开销差距是在释放锁时唤醒线程的数量,Synchronized是唤醒锁池里所有的线程+刚好来访问的线程,而 ReentrantLock则是当前线程后进来的第一个线程+刚好来访问的线程。

如果是线程并发量不大的情况下,那么 Synchronized因为自旋锁、偏向锁、轻量级锁的原因,不用将等待线程挂起,偏向锁甚至不用自旋,所以在这种情况下要比 ReentrantLock高效。

# 八、java -> class -> 执行

Java程序运行时,必须经过编译和运行两个步骤。首先将后缀名为.java的源文件进行编译,最终生成后缀名为.class的字节码文件。然后Java虚拟机将编译好的字节码文件加载到内存(这个过程被称为类加载,是由加载器完成的)然后虚拟机针对加载到内存的 java类进行解释执行,显示结果。

# 九、抽象类和接口的区别

【1】抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
【2】抽象类要被子类继承,接口要被类实现。
【3】接口只能做方法声明,抽象类中可以做方法声明,也可以做方法实现。
【4】接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
【5】抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
【6】抽象类里可以没有抽象方法,但如果一个类里有抽象方法,那么这个类只能是抽象类。
【7】抽象方法要被实现,所以不能是静态的,也不能是私有的。
【8】接口可继承接口,并可多个继承接口,但类只能单个继承。

# 十、继承和聚合的区别在哪

继承: 指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系;在 Java中此类关系通过关键字 extends明确标识,在设计时一般没有争议性;

聚合: 是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即 has-a的关系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享;比如计算机与CPU、公司与员工的关系等;表现在代码层面,和关联关系是一致的,只能从语义级别来区分;

# 十一、IO模型有哪些,讲讲你理解的 NIO ,他和 BIO,AIO的区别是啥,谈谈 Reactor模型

之前写过一篇NIO与IO:链接
之前写过一篇Reactor:链接

Reactor: 一般情况下,I/O 复用机制需要事件分发器(event dispatcher)。事件分发器:即将那些读写事件源分发给各读写事件的处理者。开发人员在开始的时候需要在分发器那里注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函数;事件分发器在适当的时候,会将请求的事件分发给这些 handler或者回调函数。

涉及到事件分发器的两种模式称为:Reactor(NIO通常采用Reactor模式)和Proactor(AIO通常采用Proactor模式)。 Reactor模式是基于同步I/O的,而 Proactor模式是和异步I/O相关的。在 Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是 socket可读写),事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。

在 Reactor中实现读: 1)、注册读就绪事件和相应的事件处理器。2)、事件分离器等待事件。3)、事件到来,激活分离器,分离器调用事件对应的处理器。4)、事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。

# 十二、反射的原理

反射原理:Java语言编译之后会生成一个.class文件,反射就是通过字节码文件找到某一个类、类中的方法以及属性等。
【1】获取类对象:通过类名获取 Class对象,Class<T> c = Class.forName("类的完全路径");通过 Class对象获取具体的类对象:Object o = (Object) c.newInstance();
【2】获取类中的构造方法:getConstructor()等方法;
【3】获取类中的属性:getFields()等方法;
【4】获取类中的方法:getMethods()等方法;

创建类实例的三种方式 :【1】调用类的 Class对象的 newInstance方法,该方法会调用对象的默认构造器,如果没有默认构造器,会调用失败。

Class<?> classType = ExtendType.class;
Object inst = classType.newInstance();
System.out.println(inst);

//输出:
/*Type:Default Constructor
 *ExtendType:Default Constructor
 *com.quincy.ExtendType@d80be3
 */
1
2
3
4
5
6
7
8
9

【2】调用默认Constructor 对象的 newInstance方法:

Class<?> classType = ExtendType.class;
Constructor<?> constructor1 = classType.getConstructor();
Object inst = constructor1.newInstance();
System.out.println(inst);

//输出:
/*Type:Default Constructor
 *ExtendType:Default Constructor
 *com.quincy.ExtendType@1006d75
 */
1
2
3
4
5
6
7
8
9
10

【3】调用带参数 Constructor 对象的 newInstance方法:

Constructor<?> constructor2 =classType.getDeclaredConstructor(int.class, String.class);
Object inst = constructor2.newInstance(1, "123");
System.out.println(inst);

//输出:
/*Type:Default Constructor
 *ExtendType:Constructor with parameters
 *com.quincy.ExtendType@15e83f9
 */
1
2
3
4
5
6
7
8
9

【详细博文链接】

# 十三、反射中 Class.forName和 ClassLoader区别

【1】Class.forName(className)方法,内部实际调用的方法是 Class.forName(className,true,classloader);第2个 boolean参数表示类是否需要初始化,Class.forName(className)默认是需要初始化。一旦初始化,就会触发目标对象的 static 块代码执行,static 参数也会被再次初始化。
【2】ClassLoader.loadClass(className)方法,内部实际调用的方法是 ClassLoader.loadClass(className,false);第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接,由上面介绍可知,不进行链接意味着不进行包括初始化等一系列步骤,那么静态块和静态对象就不会得到执行。

【classload 与 forName验证】:案例如下

//1、需要加载的目标类
public class Line {
    static {
        System.out.println("静态代码块执行: loading line");
    }
}

//2、测试类
package com.reflect;

public class ClassloaderAndForNameTest {
    public static void main(String[] args) {
        String wholeNameLine = "com.reflect.Line";
        System.out.println("下面是测试Classloader的效果");
        testClassloader(wholeNameLine);
        System.out.println("----------------------------------");
        System.out.println("下面是测试Class.forName的效果");
        testForName(wholeNameLine);
    }

    /**
     * classloader
     * @param wholeNameLine
     */
    private static void testClassloader(String wholeNameLine) {
        Class<?> line;
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        try {
            line = loader.loadClass(wholeNameLine);
            System.out.println("line " + line.getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * Class.forName
     * @param wholeNameLine
     */
    private static void testForName(String wholeNameLine) {
        try {
            Class<?> line = Class.forName(wholeNameLine);
            System.out.println("line   " + line.getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

【输出结果】: 根据运行结果,可以看到,classloader并没有执行静态代码块,如开头的理论所说。而下面的 Class.forName加载完之后,就里面执行了静态代码块,可以看到 line 静态代码块执行结果是一起的,然后才是各自的打印结果。也说明上面理论是OK的。

//被代理类
public class TargetProxy {
    public void save() {
        System.out.println("假如这个是Spring的service层的方法");
    }
}

//代理对象:
public class ProxyFactory implements MethodInterceptor {
    private Object target;

    public ProxyFactory(Object target) {
        this.target = target;
    }

    // 给目标对象创建一个代理对象
    public Object getProxyInstance() {
        Enhancer en = new Enhancer();
        en.setSuperclass(target.getClass());
                // 回调方法
        en.setCallback(this);
                // 创建代理对象
        return en.create();
    }

    @Override
       /**
        * 调用方法
        */
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("开始事务...");
                //执行方法
        Object returnValue = method.invoke(target, args);
        System.out.println("提交事务...");
        return returnValue;
    }
}

//执行测试 :
public class CglibProxy {
    public static void main(String[] args) {
        // 目标对象
        TargetProxy target = new TargetProxy();
        ProxyFactory factory = new ProxyFactory(target);
        target = (TargetProxy) factory.getProxyInstance();
        // 执行代理对象的方法
        target.save();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

【应用场景】: 在我们熟悉的 Spring框架中的 IOC的实现就是使用的 ClassLoader。而在我们使用 JDBC时通常是使用 Class.forName()方法来加载数据库连接驱动。这是因为在 JDBC规范中明确要求 Driver(数据库驱动)类必须向 DriverManager注册自己。

# 十四、描述动态代理的几种实现方式

【1】JDK动态代理: 底层封装了实现细节,格式固定,代码简单。直接调用 java.lang.reflect.Proxy静态方法 newProxyInstance即可;JDK底层是利用反射机制,需要基于接口方式,这是由于被代理的对象必须是一个类,且必须有父接口;被代理的类需要增强的方法必须在父接口中出现;

/* 1、ClassLoader loader,:指定当前目标对象使用类加载器,获取加载器的方法是固定的
 * 2、Class<?>[] interfaces,:目标对象实现的接口的类型,使用泛型方式确认类型
 * 3、InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入
 */
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )
1
2
3
4
5

【2】CGlib动态代理: 也叫作子类代理:则是基于 ASM框架,实现了无反射机制进行代理,利用空间来换取了时间,代理效率高于JDK。它是在内存中构建一个子类对象从而实现对目标对象功能的扩展,广泛的被许多 AOP的框架使用。

//被代理类
public class TargetProxy {
    public void save() {
        System.out.println("假如这个是Spring的service层的方法");
    }
}

//代理对象:
public class ProxyFactory implements MethodInterceptor {
    private Object target;

    public ProxyFactory(Object target) {
        this.target = target;
    }

    // 给目标对象创建一个代理对象
    public Object getProxyInstance() {
        Enhancer en = new Enhancer();
        en.setSuperclass(target.getClass());
                // 回调方法
        en.setCallback(this);
                // 创建代理对象
        return en.create();
    }

    @Override
       /**
        * 调用方法
        */
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("开始事务...");
                //执行方法
        Object returnValue = method.invoke(target, args);
        System.out.println("提交事务...");
        return returnValue;
    }
}

//执行测试 :
public class CglibProxy {
    public static void main(String[] args) {
        // 目标对象
        TargetProxy target = new TargetProxy();
        ProxyFactory factory = new ProxyFactory(target);
        target = (TargetProxy) factory.getProxyInstance();
        // 执行代理对象的方法
        target.save();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

JDK 动态代理的目标对象一定要实现接口,否则不能用动态代理。但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以使用 CGlib以目标对象子类的方式类实现代理。

【详细博客连接】:链接

# 十五、String.intern 方法

在 JAVA 语言中有8中基本类型和一种比较特殊的类型 String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个 JAVA系统级别提供的缓存。8种基本类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种:
【1】直接使用双引号声明出来的 String对象会直接存储在常量池中。
【2】如果不是用双引号声明的 String对象,可以使用 String提供的 intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

String#intern “如果常量池中存在当前字符串, 就会直接返回当前字符串。如果常量池中没有此字符串, 会将此字符串放入常量池中后,再返回”。

来看一段代码:

public static void main(String[] args) {
    String s = new String("1");
    s.intern();
    String s5 = s.intern();
    String s2 = "1";
    System.out.println(s5== s2); //true
    System.out.println(s == s2); //false

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4); //true
}
1
2
3
4
5
6
7
8
9
10
11
12
13

说说 jdk7 中的情况。这里要明确一点的是,在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 是会直接产生 java.lang.OutOfMemoryError: PermGen space错误的。 所以在 jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域了。为什么要移动,Perm 区域太小是一个主要原因,当然据消息称 jdk8 已经直接取消了 Perm 区域,而新建立了一个元区域。应该是 jdk 开发者认为 Perm 区域已经不适合现在 JAVA 的发展了。

正式因为字符串常量池移动到 JAVA Heap 区域后,再来解释为什么会有上述的打印结果。

JAVA基础

【1】在第一段代码中,先看 s3和s4字符串。String s3 = new String("1") + new String("1");这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的对象。中间还有2个匿名的new String("1")我们不去讨论它们。此时s3引用对象内容是”11”,但此时常量池中是没有 “11”对象的。
【2】接下来s3.intern();这一句代码,是将 s3中的“11”字符串放入 String 常量池中,因为此时常量池中不存在“11”字符串,在常量池中生成一个 “11” 的对象,关键点是 jdk7 中常量池不在 Perm 区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。
【3】最后String s4 = "11"; 这句代码中”11”是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。所以 s4 引用就指向和 s3 一样了。因此最后的比较 s3 == s4 是 true。
【4】再看 s 和 s2 对象。 String s = new String("1"); 第一句代码,生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。s.intern(); 这一句是 s 对象去常量池中寻找后发现 “1” 已经在常量池里了。
【5】接下来String s2 = "1"; 这句代码是生成一个 s2的引用指向常量池中的“1”对象。 结果就是 s 和 s2 的引用地址明显不同。图中画的很清晰。

# 十六、i++ 操作

1、i++有三项操作,将值赋给中间变量int temp=i;i=i+1;return i;
2、i=i++有四项操作,将值赋给中间变量int temp=i;i=i+1;i=temp;return i;

# 十七、final 的用途

final 在 Java中是一个保留的关键字,可以声明 成员变量、方法、类以及本地变量。一旦你将引用声明作 final,你将不能改变这个引用了,编译器会检查代码,如果你试图将变量再次初始化的话,编译器会报编译错误。
【1】final变量:凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为 final的都叫作 final变量。final变量经常和 static关键字一起使用,作为常量。不允许被修改。
【2】final方法:方法前面加上 final关键字,代表这个方法不可以被子类的方法重写。final方法比非 final方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。
【3】final类:final 修饰的类通常功能是完整的,它不能被继承。Java 中有许多类是 final的,譬如String,Interger以及其他包装类。

# 十八、写出三种单例模式实现

【1】懒汉式单例模式,线程不安全。

//这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
public class Singleton {
    private static Singleton instance;
    private Singleton (){}

    public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

【2】饿汉式单例模式

/*
 *这种方式基于classloder机制避免了多线程的同步问题,instance在类装载时就实例化。目前java单例是指一
 *个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机在通过自己的ClassLoader装载饿汉式实现单
 *例类的时候就会创建一个类的实例。这就意味着一个虚拟机里面有很多ClassLoader,而这些classloader都能
 *装载某个类的话,就算这个类是单例,也能产生很多实例。当然如果一台机器上有很多虚拟机,那么每个虚拟机
 *中都有至少一个这个类的实例的话,那这样 就更不会是单例了。(这里讨论的单例不适合集群!)
 */
public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton (){}
    public static Singleton getInstance() {
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

【3】静态内部类

/*这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,这种方式是Singleton类被
 *装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,
 *才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载!这个时候,这种方式相比第2种方式就显得很合理。
 */
public class Singleton {
    private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static final Singleton getInstance() {
    return SingletonHolder.INSTANCE;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

【4】枚举方式

/*这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反
 *序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于1.5中才加入enum特性,用这种方式写
 *不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。
 */
public enum Singleton {
    INSTANCE;
    public void whateverMethod() {
    }
}
1
2
3
4
5
6
7
8
9

【5】双重校验锁

/*这样方式实现线程安全地创建实例,而又不会对性能造成太大影响。它只是第一次创建实例的时候同步,以后就不需要同步了。
 *由于volatile关键字屏蔽了虚拟机中一些必要的代码优化,所以运行效率并不是很高,因此建议没有特别的需要不要使用。双重检验锁方式的单例不建议大量使用,根据情况决定。
 */
public class Singleton {
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
    if (singleton == null) {
        synchronized (Singleton.class) {
        if (singleton == null) {
            singleton = new Singleton();
        }
        }
    }
    return singleton;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

【博客链接】:链接

# 十九、String s = new String("xyz"); 创建了几个字符串对象

两个对象,一个是静态区(即方法区)的"xyz",一个是用 new创建在堆上的对象,还有一个引用放在栈上。对于上面使用 new创建的字符串对象,如果想将这个对象的引用加入到字符串常量池,可以使用 intern方法:String s2 =s.intern(); intern()返回字符串对象的规范化表示形式。一个初始为空的字符串池,它由类 String 私有地维护。当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。

new String(xx) 和 string = xxx的区别

String s1 = "test";
String s2 = "test";
System.out.println("s1和s2是同一个字符串" + (s1 == s2));//true

String s3 = new String("test");
System.out.println("s1和s3是同一个字符串" + (s1 == s3));//false
String s4 = "tes" + "t";
System.out.println("s1和s4是同一个字符串" + (s1 == s4));//true
System.out.println("s3和s4是同一个字符串" + (s3 == s4));//false

String s5 = new String("test");
System.out.println("s1和s5是同一个字符串" + (s1 == s5));//false
System.out.println("s3和s5是同一个字符串" + (s3 == s5));//false

String s6 = s3.intern();
System.out.println("s1和s6是同一个字符串" + (s1 == s6));//true
System.out.println("s3和s6是同一个字符串" + (s3 == s6));//false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 二十、修饰符public、private、protected、default的作用

同一个类 同一个包 不同包的子类 不同包的非子类
Private
Default
Protected
Public

【1】Public: Java 语言中访问限制最宽的修饰符,一般称之为“公共的”。被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包(package)访问。
【2】Private: Java语言中对访问权限限制的最窄的修饰符,一般称之为“私有的”。被其修饰的类、属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问。
【3】Protect: 介于public 和 private 之间的一种访问修饰符,一般称之为“保护形”。被其修饰的类、属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。
【4】default: 即不加任何访问修饰符,通常称为“默认访问模式“。该模式下,只允许在同一个包中进行访问。

# 二十一、深拷贝和浅拷贝区别

【1】浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间。
【2】深拷贝(.clone())不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

# 二十二、数组和链表数据结构描述,各自的时间复杂度

【1】数组: 是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素。但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样的道理,如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素。如果应用需要快速访问数据,很少插入和删除元素,就应该用数组。数组从栈中分配空间,对于程序员方便快速,但自由度小。数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。

【2】链表: 元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起,每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针。如果要访问链表中一个元素,需要从第一个元素开始,一直找到需要的元素位置。但是增加和删除一个元素对于链表数据结构就非常简单了,只要修改元素中的指针就可以了。如果应用需要经常插入和删除元素你就需要用链表。链表从堆中分配空间,自由度大但申请管理比较麻烦。链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。

【3】时间复杂度: 数组利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n);数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)。

# 二十三、Error 和 Exception的区别,CheckedException,RuntimeException的区别

JAVA基础

Error: 当程序发生不可控的错误时,通常做法是通知用户并中止程序的执行。与异常不同的是 Error及其子类的对象不应被抛出。Error 是 Throwable的子类,代表编译时间和系统错误,用于指示合理的应用程序不能捕获的严重问题。Error由Java 虚拟机生成并抛出,包括动态链接失败,虚拟机错误等。程序对其不做处理。

Exception: 一般分为 Checked异常和 Runtime异常,所有 RuntimeException类及其子类的实例被称为 Runtime异常,不属于该范畴的异常则被称为 CheckedException。
【1】Checked 异常: 只有 java语言提供了 Checked异常,Java 认为 Checked异常都是可以被处理的异常,所以 Java程序必须显示处理 Checked异常。如果程序没有处理 Checked异常,该程序在编译时就会发生错误无法编译。这体现了 Java的设计哲学:没有完善错误处理的代码根本没有机会被执行。对 Checked异常处理方法有两种:①、当前方法知道如何处理该异常,则用 try-catch块来处理该异常。②、当前方法不知道如何处理,则在定义该方法是声明抛出该异常。我们比较熟悉的Checked异常有:

Java.lang.ClassNotFoundException
Java.lang.NoSuchMetodException
Java.io.IOException
1
2
3

【2】RuntimeException: 如除数是0和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。我们比较熟悉的 RumtimeException类的子类有:

Java.lang.ArithmeticException
Java.lang.ArrayStoreExcetpion
Java.lang.ClassCastException
Java.lang.IndexOutOfBoundsException
Java.lang.NullPointerException
1
2
3
4
5

# 二十四、请列出5个运行时异常

ClassCastException 类转换异常
IllegalArgumentException 非法参数异常
IndexOutOfBoundsException 下标越界异常
ArithmeticException 算术运算异常
OutOfMemoryError 内存不足
StackOverflowError 堆栈溢出
ClassNotFoundException 找不到类异常
InterruptedException 终止异常

# 二十五、类加载器加载机制

如果自己创建一个 String类是不会被加载的。原因:类加载器的委托机制。类的加载过程采用父亲委托机制。这种机制能更好的保证 java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当 Java程序请求加载器 loader加载 Sample类时,loader首先委托自己的父加载器去加载 Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器 loader本身加载 Sample类。

JAVA基础

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到 BootStrap ClassLoader逐层检查,只要某个 Classloader已加载就视为已加载此类,保证此类只加载一次。而加载的顺序是自顶向下,也就是说当发现这个类没有的时候会先去让自己的父类去加载。那么例子中我们自己写的 String应该是被 Bootstrap ClassLoader加载了,所以App ClassLoader就不会再去加载我们写的 String类了,导致我们写的 String类是没有被加载的。

# 二十六、Object对象中 hashCode和 equals方法的理解

【1】hashCode: hashCode是 jdk根据对象的地址或者字符串或者数字算出来的 int类型的数值,也就是哈希码,哈希码并不是完全唯一的,它是一种算法,让同一个类的对象按照自己不同的特征尽量的有不同的哈希码,但不表示不同的对象哈希码完全不同。
【2】equals: 在 Object这个类里面提供的 Equals()方法默认的实现是比较当前对象的引用和你要比较的那个引用它们指向的是否是同一个对象。
【3】当我们需要对值进行比较时,就需要重写这两个方法。例如 String就重写了这两个方法,比较的是 value值是否相等。

TIP

当使用 HashSet或 HashMap时,hashCode方法就会被调用,判断已经存储在集合中对象的 hashCode值是否与要加入到集合中的 hashCode值是否相同,如果不相同,则把元素加入进去,如果一致,再进行 equals方法的比较,分成两种情况:
  a,如果 equals方法返回true,表示对象已经在集合之中,不会加入
  b,如果 equals方法返回false,表示对象不在集合之中,就会加入

所以,我们要是重写了hashCode方法也要重写equals方法,反之亦然

# 二十七、泛型的作用

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,能够解决代码复用的问题。常见的一种情况是,你有一个函数,它带有一个参数,参数类型是A,然而当参数类型改变成 B的时候,你不得不复制这个函数。除此之外泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,消除显示的类型强制转换,以提高代码的重用率。使用方法:

public class Stack<T>{
        private T[] m_item;
        public T Pop(){...}
        public void Push(T item){...}
        public Stack(int i)
        {
            this.m_item = new T[i];
        }
}
1
2
3
4
5
6
7
8
9

类的写法不变,只是引入了通用数据类型T就可以适用于任何数据类型,并且类型安全的。这个类的调用方法:

//实例化只能保存int类型的类
Stack<int> a = new Stack<int>(100);
      a.Push(10);
      a.Push("8888"); //这一行编译不通过,因为类a只接收int类型的数据
      int x = a.Pop();

//实例化只能保存string类型的类
Stack<string> b = new Stack<string>(100);
      b.Push(10);    //这一行编译不通过,因为类b只接收string类型的数据
      b.Push("8888");
      string y = b.Pop();
1
2
3
4
5
6
7
8
9
10
11

# 二十八、这样的 a.hashcode() 有什么用,与 a.equals(b)有什么关系

hashcode()方法提供了对象的 hashCode值,是一个 native方法,返回的默认值与 System.identityHashCode(obj)一致。通常这个值是对象头部的一部分二进制位组成的数字,具有一定的标识对象的意义存在,但绝不定于地址。

作用是: 用一个数字来标识对象。比如在 HashMap、HashSet 等类似的集合类中,如果用某个对象本身作为Key,即要基于这个对象实现 Hash的写入和查找,那么对象本身如何实现这个呢?就是基于 hashcode这样一个数字来完成的,只有数字才能完成计算和对比操作。

equals 与 hashcode的关系: equals 相等的两个对象,则重写后的 hashcode一定要相等。但是 hashcode相等的两个对象 equals 不一定相等。

# 二十九、有没有可能2个不相等的对象有相同的 hashcode

hashCode 是所有 java对象的固有方法,如果不重载的话,返回的实际上是该对象在 jvm的堆上的内存地址,而不同对象的内存地址肯定不同,所以这个 hashCode也就肯定不同了。如果重载了的话,由于采用的算法的问题,有可能导致两个不同对象的 hashCode相同。hashcode()方法返回一个 int数,在 Object类中的默认实现是“将该对象的内部地址转换成一个整数返回”。接下来有两个个关于这两个方法的重要规范(我只是抽取了最重要的两个,其实不止两个):

【1】若重写 equals(Object obj)方法,有必要重写 hashcode()方法,确保通过 equals(Object obj)方法判断结果为 true的两个对象具备相等的 hashcode()返回值。说得简单点就是:“如果两个对象相同,那么他们的 hashcode应该相等”。不过请注意:这个只是规范,如果你非要写一个类让 equals(Object obj) 返回 true而 hashcode()返回两个不相等的值,编译和运行都是不会报错的。不过这样违反了Java规范,程序也就埋下了BUG。

【2】如果 equals(Object obj)返回 false,即两个对象“不相同”,并不要求对这两个对象调用 hashcode()方法得到两个不相同的数。说的简单点就是:“如果两个对象不相同,他们的 hashcode可能相同”。

# 三十、Java 中的 HashSet 内部是如何工作的

它是基于 HashMap 实现的,底层采用 HashMap 来保存元素。所有 Set接口的类内部都是由 Map做支撑的。HashSet 用HashMap 对它的内部对象进行排序。你一定好奇输入一个值到 HashMap,我们需要的是一个键值对,但是我们传给 HashSet的是一个值。实际上我们插入到 HashSet中的值在 Map对象中起的是键的作用,因为它的值Java用了一个常量。所以在键值对中所有的键的值都是一样的。

class Test
{
    public static void main(String[]args)
    {
        HashSet<String> h = new HashSet<String>();

        // adding into HashSet
        h.add("India");
        h.add("Australia");
        h.add("South Africa");
        h.add("India");// adding duplicate elements

        // printing HashSet
        System.out.println(h);//[Australia, South Africa, India]
        System.out.println("List contains India or not:" +
                           h.contains("India"));//true

        // Removing an item
        h.remove("Australia");
        System.out.println("List after removing Australia:"+h);//[South Africa, India]

        // Iterating over hash set items
        System.out.println("Iterating over list:");
        Iterator<String> i = h.iterator();
        while (i.hasNext())
            System.out.println(i.next());//South Africa  India
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 三十一、序列化与反序列化

序列化 (Serialization): 将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

序列化实现:
 1)、Serializable 接口:类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
 2)、Externalizable接口:除了Serializable 之外,java 中还提供了另一个序列化接口 Externalizable。Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写 writeExternal()与 readExternal()方法。
 3)、ObjectOutput和ObjectInput 接口。
 4)、ObjectOutputStream类和 ObjectInputStream类。

注意:
 1)、Transient关键字:作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
 2)、序列化ID:虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)。序列化 ID 在 Eclipse 下提供了两种生成策略,一个是固定的 1L,一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。

为什么序列化:
 1)、当你想把的内存中的对象保存到一个文件中或者数据库中时候;
 2)、当你想用套接字在网络上传送对象的时候;
 3)、当你想通过 RMI传输对象的时候;

反序列化问题及解决办法:
 1)、类里面一定要 serialVersionUID,否则旧数据会反序列化会失败。 解决方法: serialVersionUID 是根据该类名、方法名等数据生产的一个整数,用来验证版本是否一致。如果不加这个字段,当你的类修改了字段,在反序列化的时候会直接报异常:InvalidCastException,导致无法完成反序列化。
 2)、一旦序列化保存到磁盘操作后,就不要修改类名了,否则旧数据会反序列化会失败。 解决方法: 所以尽量把对象转换成 JSON保存更稳妥。

# 三十二、java8 的新特性

【1】Lambda 表达式: 将函数式编程引入了Java。Lambda 允许把函数作为一个方法的参数,或者把代码看成数据。

String[] atp = {"Nadal", "Djokovic",  "Wawrinka"};
List<String> players =  Arrays.asList(atp);

// 以前的循环方式
for (String player : players) {
     System.out.print(player + "; ");
}
// 使用 lambda 表达式以及函数操作(functional operation)
players.forEach((player) -> System.out.print(player + "; "));
1
2
3
4
5
6
7
8
9

【2】接口的默认方法与静态方法:我们可以在接口中定义默认方法,使用 default关键字,并提供默认的实现。所有实现这个接口的类都会接受默认方法的实现,除非子类提供的自己的实现。

public interface DefaultFunctionInterface {
    default String defaultFunction() {
        return "default function";
    }
}

//我们还可以在接口中定义静态方法,使用static关键字,也可以提供实现。
public interface StaticFunctionInterface {
    static String staticFunction() {
        return "static function";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

【3】方法引用:通常与 Lambda表达式联合使用,可以直接引用已有 Java类或对象的方法。
【4】重复注解:在 Java 5中使用注解有一个限制,即相同的注解在同一位置只能声明一次。Java 8 引入重复注解,这样相同的注解在同一地方也可以声明多次。重复注解机制本身需要用@Repeatable注解。Java 8在编译器层做了优化,相同注解会以集合的方式保存,因此底层的原理并没有变化。
【5】扩展注解的支持:Java 8 扩展了注解的上下文,几乎可以为任何东西添加注解,包括局部变量、泛型类、父类与接口的实现,连方法的异常也能添加注解。
【6】Optional:Java 8 引入 Optional 类来防止空指针异常,Optional 类最先是由 Google 的 Guava项目引入的。Optional类实际上是个容器:它可以保存类型T的值,或者保存null。使用Optional类我们就不用显式进行空指针检查了。

Optional<String> str = Optional.of("test");
//1、判断str是否为null,如果不为null,则为true,否则为false
if (str.isPresent()) {
    //get用于获取变量的值,当变量不存在的时候会抛出NoSuchElementException,如果不能确定变量一定存在值,则不推荐使用
    str.get();
}
1
2
3
4
5
6

【7】Stream:Stream API 是把真正的函数式编程风格引入到 Java中。其实简单来说可以把 Stream理解为 MapReduce,当然Google 的 MapReduce的灵感也是来自函数式编程。她其实是一连串支持连续、并行聚集操作的元素。从语法上看,也很像 Linux的管道、或者链式编程,代码写起来简洁明了,非常酷帅!
【8】Date/Time API (JSR 310):Java 8 新的 Date-Time API (JSR 310)受Joda-Time的影响,提供了新的 java.time包,可以用来替代 java.util.Date和 java.util.Calendar。一般会用到 Clock、LocaleDate、LocalTime、LocaleDateTime、ZonedDateTime、Duration这些类,对于时间日期的改进还是非常不错的。
【9】JavaScript 引擎 Nashorn:Nashorn允许在 JVM上开发运行 JavaScript应用,允许 Java与 JavaScript相互调用。
【10】Base64:在 Java 8中,Base64 编码成为了Java类库的标准。Base64 类同时还提供了对URL、MIME友好的编码器与解码器。 【更多细节】:链接

# 三十三、sort() 底层使用的是什么算法

collections.sort 方法底层就是调用的 Arrays.sort() 方法,而 Array.sort 使用了两种排序方法,快速排序和优化的归并排序。快速排序主要是对那些基本数据类型(int,short、long等)排序,而归并排序用于对 Object 类型进行排序。使用不同类型的排序算法主要是由于快速排序是不稳定的,而归并排序是稳定的。这里的稳定是指当比较相等的数据时,会按照原有的数据顺序排列。对于基本数据类型,稳定性没有意义,而对于 Object 类型,稳定性是比较重要的,因为对象相等的判断可能只是判断关键属性,最好保存相等对象的非关键属性的顺序与排序前一致;另外一个原因是由于归并排序相对而言比较次数比快速排序少,对象引用的移动次数比快速排序多,而对于对象而言,比较一般比移动耗时。此外,对于数组排序。快速排序的 sort() 采用递归实现,数组规模太大时会出现堆栈溢出,而归并排序 sort() 采用非递归排序,不存在此问题。

总结: 首先判断需要排序的数据量是否大于 60;
【1】小于60:使用插入排序,插入排序是稳定的;
【2】大于60:数据量会根据数据类型选择排序方式:基本类型使用快速排序。因为基本类型都是指向同一个常量池不需要考虑稳定性。Object 类型:使用归并排序。因为归并排序具有稳定性。

注意: 不管是排序排序还是归并排序。在二分的时候小于60的数据量依旧会使用插入排序。

【jdk1.8】:进入 Arrays.sort(s); 源代码:

static void sort(int[] a, int left, int right,
                 int[] work, int workBase, int workLen) {
    // 对小数组使用快速排序 QUICKSORT_THRESHOLD = 286
    if (right - left < QUICKSORT_THRESHOLD) {
        sort(a, left, right, true);
        return;
    }
    //......
}
1
2
3
4
5
6
7
8
9

数组一进来,会碰到第一个阀值QUICKSORT_THRESHOLD(286),注解上说,小过这个阀值的进入Quicksort (快速排序),其实并不全是,点进去 sort(a, left, right, true); 方法:

for (int i = left, j = i; i < right; j = ++i) {
    int ai = a[i + 1];
    while (ai < a[j]) {
        a[j + 1] = a[j];
        if (j-- == left) {
            break;
        }
    }
    a[j + 1] = ai;
}
1
2
3
4
5
6
7
8
9
10

【插入排序博客】:链接

至于大过 INSERTION_SORT_THRESHOLD(47)的,用一种快速排序的方法:
【1】从数列中挑出五个元素,称为 “基准”(pivot);
【2】重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
【3】递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

【快速排序博客】:链接

这是少于阀值QUICKSORT_THRESHOLD(286)的两种情况,至于大于286的,它会进入归并排序(Merge Sort),但在此之前,它有个小动作:

// 检查数组是否接近排序
for (int k = left; k < right; run[count] = k) {
    if (a[k] < a[k + 1]) { // ascending
        while (++k <= right && a[k - 1] <= a[k]);
    } else if (a[k] > a[k + 1]) { // descending
        while (++k <= right && a[k - 1] >= a[k]);
        for (int lo = run[count] - 1, hi = k; ++lo < --hi; ) {
            int t = a[lo]; a[lo] = a[hi]; a[hi] = t;
        }
    } else { // equal
        for (int m = MAX_RUN_LENGTH; ++k <= right && a[k - 1] == a[k]; ) {
            if (--m == 0) {
                sort(a, left, right, true);
                return;
            }
        }
    }
    /*
     * 阵列结构不高,使用快速排序而不是合并排序。
     */
    if (++count == MAX_RUN_COUNT) {
        sort(a, left, right, true);
        return;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

这里主要作用是看他数组具不具备结构:实际逻辑是分组排序,每降序为一个组,像1,9,8,7,6,8。9到6是降序,为一个组,然后把降序的一组排成升序:1,6,7,8,9,8。然后最后的8后面继续往后面找。每遇到这样一个降序组,++count,当 count大于MAX_RUN_COUNT(67),被判断为这个数组不具备结构(也就是这数据时而升时而降),然后送给之前的 sort(里面的快速排序 )的方法(The array is not highly structured,use Quicksort instead of merge sort.)。如果 count 少于MAX_RUN_COUNT(67)的,说明这个数组还有点结构,就继续往下走下面的归并排序:

// 确定合并的替换基
byte odd = 0;
for (int n = 1; (n <<= 1) < count; odd ^= 1);
// 使用或创建用于合并的临时数组b
int[] b;                 // temp array; alternates with a
int ao, bo;              // array offsets from 'left'
int blen = right - left; // space needed for b
if (work == null || workLen < blen || workBase + blen > work.length) {
    work = new int[blen];
    workBase = 0;
}
if (odd == 0) {
    System.arraycopy(a, left, work, workBase, blen);
    b = a;
    bo = 0;
    a = work;
    ao = workBase - left;
} else {
    b = work;
    ao = 0;
    bo = workBase - left;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

从这里开始,正式进入归并排序(Merge Sort)!

// Merging
for (int last; count > 1; count = last) {
    for (int k = (last = 0) + 2; k <= count; k += 2) {
        int hi = run[k], mi = run[k - 1];
        for (int i = run[k - 2], p = i, q = mi; i < hi; ++i) {
            if (q >= hi || p < mi && a[p + ao] <= a[q + ao]) {
                b[i + bo] = a[p++ + ao];
            } else {
                b[i + bo] = a[q++ + ao];
            }
        }
        run[++last] = hi;
    }
    if ((count & 1) != 0) {
        for (int i = right, lo = run[count - 1]; --i >= lo;
            b[i + bo] = a[i + ao]
        );
        run[++last] = right;
    }
    int[] t = a; a = b; b = t;
    int o = ao; ao = bo; bo = o;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

总结: 从上面分析,Arrays.sort 并不是单一的排序,而是插入排序,快速排序,归并排序三种排序的组合,为此我画了个流程图:

JAVA基础

O(nlogn)只代表增长量级,同一个量级前面的常数也可以不一样,不同数量下面的实际运算时间也可以不一样。数量非常小的情况下(就像上面说到的,少于47的),插入排序等可能会比快速排序更快。所以数组少于47的会进入插入排序。

快排数据越无序越快(加入随机化后基本不会退化),平均常数最小,不需要额外空间,不稳定排序。归排速度稳定,常数比快排略大,需要额外空间,稳定排序。所以大于或等于47或少于286会进入快排,而在大于或等于286后,会有个小动作:“// Check if the array is nearly sorted”。这里第一个作用是先梳理一下数据方便后续的归并排序,第二个作用就是即便大于286,但在降序组太多的时候(被判断为没有结构的数据,The array is not highly structured,use Quicksort instead of merge sort.),要转回快速排序。

# 三十四、常见算法的复杂度是多少

JAVA基础

# 三十五、int 与 Integer 之间的数据比较

Integer i1=59;
int i2=59;
Integer i3=Integer.valueOf(59);
Integer i4=new Integer(59);
System.out.println(i2==i1); // true
System.out.println(i3==i4); // false
System.out.println(i2==i4); // true
System.out.println(i1==i3); // true
1
2
3
4
5
6
7
8

调试可看到:i1,i3 的地址值是一样的,i4地址不同。是因为直接使用 Integer包装类进行赋值的话,会调用常量池中的对象,是不会产生新对象的。而用构造方法的话,就会新开辟一个堆空间。Integer 和 int 进行比较的话,会自动拆装箱,所以值是一样的。

JAVA基础

【若值不在常量池 [-128,127] 之间】: 调试可得,我们看到,i1,i3地址值也不一样了,为什么呢,这里我们说,当值超过[-128,127]范围时,就会申请在堆中 new一个对象。

Integer i1=128;
int i2=128;
Integer i3=Integer.valueOf(128);
Integer i4=new Integer(128);
System.out.println(i2==i1); //true
System.out.println(i3==i4); //false
System.out.println(i2==i4); //true
System.out.println(i1==i3); //false
1
2
3
4
5
6
7
8

总结:
 ① Integer 直接赋值时,若值在[-128,127] 之间则不会申请新对象,会调用常量池中的对象;  ② 若超过范围,则申请 new一个对象;  ③ 若采用构造方法赋值,则在堆上开辟新空间;  ④ Integer和 int进行 ==比较时,由于会自动拆箱,将 Integer转为 int,则直接看值的大小就可以。

【源码展示】:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
1
2
3
4
5

# 三十六、函数式编程与面向对象编程的区别

【1】函数式编程(functional programming): 又称泛函编程,是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。函数编程语言最重要的基础是λ演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。

【函数式编程的优点】: 在函数式编程中,由于数据全部都是不可变的,所以没有并发编程的问题,是多线程安全的。可以有效降低程序运行中所产生的副作用,对于快速迭代的项目来说,函数式编程可以实现函数与函数之间的热切换而不用担心数据的问题,因为它是以函数作为最小单位的,只要函数与函数之间的关系正确即可保证结果的正确性。函数式编程的表达方式更加符合人类日常生活中的语法,代码可读性更强。实现同样的功能函数式编程所需要的代码比面向对象编程要少很多,代码更加简洁明晰。函数式编程广泛运用于科学研究中,因为在科研中对于代码的工程化要求比较低,写起来更加简单,所以使用函数式编程开发的速度比用面向对象要高很多,如果是对开发速度要求较高但是对运行资源要求较低同时对速度要求较低的场景下使用函数式会更加高效。

【函数式编程的缺点】: 由于所有的数据都是不可变的,所以所有的变量在程序运行期间都是一直存在的,非常占用运行资源。同时由于函数式的先天性设计导致性能一直不够。虽然现代的函数式编程语言使用了很多技巧比如惰性计算等来优化运行速度,但是始终无法与面向对象的程序相比,当然面向对象程序的速度也不够快。函数式编程虽然已经诞生了很多年,但是至今为止在工程上想要大规模使用函数式编程仍然有很多待解决的问题,尤其是对于规模比较大的工程而言。如果对函数式编程的理解不够深刻就会导致跟面相对象一样晦涩难懂的局面。

【2】面向对象编程(Object-oriented programming): 是种具有对象概念的程序编程范型,同时也是一种程序开发的方法。它可能包含数据、属性、代码与方法。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象。

对象与对象之间的关系是面向对象编程首要考虑的问题,而在函数式编程中,所有的数据都是不可变的,不同的函数之间通过数据流来交换信息,函数作为FP中的一等公民,享有跟数据一样的地位,可以作为参数传递给下一个函数,同时也可以作为返回值。

【面向对象编程的优点】: 面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反。传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对电脑下达的指令。面向对象程序设计中的每一个对象都应该能够接收数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。目前已经被证实的是,面向对象程序设计推广了程序的灵活性和可维护性,并且在大型项目设计中广为应用。此外,支持者声称面向对象程序设计要比以往的做法更加便于学习,因为它能够让人们更简单地设计并维护程序,使得程序更加便于分析、设计、理解。同时它也是易拓展的,由于继承、封装、多态的特性,自然设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低。在面向对象编程的基础上发展出来的23种设计模式广泛应用于现今的软件工程中,极大方便了代码的书写与维护。建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

【面向对象编程的缺点】: 面向对象编程以数据为核心,所以在多线程并发编程中,多个线程同时操作数据的时候可能会导致数据修改的不确定性。在现在的软件工程中,由于面向对象编程的滥用,导致了很多问题。首先就是为了写可重用的代码而产生了很多无用的代码,导致代码膨胀,同时很多人并没有完全理解面向对象思想,为了面向对象而面向对象,使得最终的代码晦涩难懂,给后期的维护带来了很大的问题。所以对于大项目的开发,使用面向对象会出现一些不适应的情况。面向对象虽然开发效率高但是代码的运行效率比起面向过程要低很多,这也限制了面向对象的使用场景不能包括那些对性能要求很苛刻的地方。

# 三十七、equals 和 ==区别, 重写 equals一定要重写 hashcode方法吗?为什么? hashcode方法有什么作用?

对于基本类型来说 ,==比较两个基本类型的值是否相等,对于引用类型来说,==比较的是内个引用类型的内存地址。

equals 说明:equals 用来比较的是两个对象的内容是否相等,由于所有的类都是继承自 java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖,调用的仍然是 Object类中的方法,而 Object中的 equals方法返回的却是 ==的判断。【重写 equals一般是要重写 hashcode方法的,首先 equals与 hashcode间的关系如下】:
 1)、如果两个对象相同(即用 equals比较返回 true),那么它们的 hashCode值一定要相同;  2)、如果两个对象的 hashCode相同,它们并不一定相同(即用 equals比较返回 false) ;

比如说两个字符串的 hashcode相同,但是这两个字符串可以是不同的字符串,对象也是同理。

【至于 hashcode有什么用】: 为了提高程序的效率才实现了 hashcode方法,先进行 hashcode的比较,如果不同,那没就不必在进行 equals的比较了,这样就大大减少了 equals比较的次数,很大程度上提高了比较的效率,一个很好的例子就是在集合中的使用;

# 三十八、Java序列化,有ID和没ID会出现问题吗

【序列化 ID 问题】: 两个客户端 A 和 B 试图通过网络传递对象数据,A 端将对象 C 序列化为二进制数据再传给 B,B 反序列化得到 C。

【问题】: C 对象的全类路径假设为 com.yintong.UserInfo,在 A 和 B 端都有这么一个类文件,功能代码完全一致。也都实现了 Serializable 接口,但是反序列化时总是提示不成功。

【解决】: 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。清单 1 中,虽然两个类的功能代码完全一致,但是序列化 ID 不同,他们无法相互序列化和反序列化。

# 三十九、Static 关键字的作用

【1】Static方法: 一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。但是要注意的是,虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的。

【2】Static变量: 也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。static成员变量的初始化顺序按照定义的顺序进行初始化。

【3】Static代码块: static关键字还有一个比较关键的作用就是用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照 static块的顺序来执行每个static块,并且只会执行一次。为什么说static块可以用来优化程序性能,是因为它的特性,只会在类加载的时候执行一次。

# 四十、& 和 &&的区别

【1】& 和 &&都可以用作逻辑与的运算符,表示逻辑与(and),当运算符两边的表达式的结果都为 true时,整个运算结果才为true,否则,只要有一方为 false,则结果为 false。
【2】&& 还具有短路的功能,即如果第一个表达式为 false,则不再计算第二个表达式,例如,对于 if(str != null && !str.equals(“”)) 表达式,当 str为 null时,后面的表达式不会执行,所以不会出现 NullPointerException如果将 &&改为 &,则会抛出 NullPointerException异常。
【3】& 还可以用作位运算符,当 &操作符两边的表达式不是 boolean类型时,&表示按位与操作。

0&0=0;   
0&1=0;    
1&0=0;     
1&1=1;
1
2
3
4

# 四十一 、Servlet 的生命周期

Servlet 的生命周期分为:加载阶段、实例化阶段、初始化阶段、服务阶段、销毁阶段。最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。

JAVA基础
public interface Servlet {
        //初始化方法
    void init(ServletConfig var1) throws ServletException;
        //获取ServletConfig对象,ServletConfig对象可以获取到Servlet的一些信息,ServletName、
        //ServletContext、InitParameter、InitParameterNames、通过查看ServletConfig这个接口就可以知道
    ServletConfig getServletConfig();
        //服务阶段
    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
    String getServletInfo();
        //销毁方法
    void destroy();
}
1
2
3
4
5
6
7
8
9
10
11
12

专注 HTTP请求的 Servlet,项目中比较常用:

/**
 * HttpServlet详解
 * @author 69301 doGet是给get方式的http的请求做相应的 doPost是给post方式的http的请求做相应的
 */
public class HttpServletDemo extends HttpServlet {

       @Override
       public void init() throws ServletException {
              System.out.println("实例被创建了");
       }

       @Override
       protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              System.out.println("doGet方法被调用了");
              resp.getOutputStream().write("doGet方法被调用".getBytes());
       }

       @Override
       protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              System.out.println("doPost方法被调用了");
              doGet(req, resp);
       }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(adsbygoogle = window.adsbygoogle || []).push({});