Java基础知识梳理(详细)
1. Java中的数据类型
-
基本数据类型特征表
类型 位数 最小值 最大值 默认值 其他 byte 8 -128(-2^7) 127(2^7-1) 0 有符号、二进制补码表示 short 16 -32768(-2^15) 32767(2^15-1) 0 有符号、二进制补码表示 int 32 -2^31 2^31-1 0 有符号、二进制补码表示 long 64 -2^63 2^63-1 0L(0l) 有符号、二进制补码表示 float 32 2^(-149) 2^128-1 0.0f 单精度、IEEE754标准 double 64 2^(-1074) 2^1024-1 0.0d 双精度、IEEE754标准 char 16 \u0000(0) \uffff(65535) \u0000(0) 单一的、Unicode字符 -
字符类型涉及到编码问题。
char类型固定为16位2字节长,因为java内码表示字符按照UTF-16存储。而一旦转化为字节就要根据特定的编码格式来看了,例如UTF-8占用1-4字节。出现乱码问题需要分析编码,在windows系统下默认的编码是GBK,使用gradle命令行可能出现乱码(还没发现啥解决办法)。关于字符的一个问题是char是否能表示所有的汉字,我觉得是不能的,可能可以表示常用的所有汉字,但生僻字16位肯定不够的,不然也不至于需要UTF-8多达4个字节来表示汉字。
String内部是通过char数组实现的,生僻字可能需要两个char来表示。
Unicode是一种编码规范,有Unicode编码表,其具体实现有UTF-8, UTF-16, UTF-32等。除此之外还有GBK,GB2312等。
ASCII编码占8位,一个字节就可以表示英文环境所有字符。
引申到MySQL建表过程中需要指定编码,如果是utf8编码可以留意下,一个字符占据可变字节长度,varchar(10)表示的是10个字符而非字节。
-
GB2312收录了约7000常用汉字,GBK收录了20000+汉字。相关的文章很多,也不是很容易分辨,这里我只列一下结论:GB2312和GBK都继承自ASCII编码,即英文和符号编码和ASCII一致;对于汉字的编码则一律用2字节,能够表示的范围有所不同。而UTF-8编码是1-4字节,汉字绝大多数用3字节表示。
-
浮点数内存结构
类型 位数 符号位 指数位 尾数位 float 32 1 8 23 double 64 1 11 52 -
原始数据类型对应的封装对象
- (byte, Byte), (short, Short), (long, Long), (float,Float), (double, Double), (boolean, Boolean)
- (int, Integer), (char, Character)
【小题】
Integer i = null; int j = i.intValue();
【答案】编译通过,但运行时报错NullPointerException。
-
自动装箱和拆箱
Integer i = 100; //自动装箱,编译器执行Integer.valueOf(100) int j = i; //自动拆箱,编译器执行i.intValue()
由于自动拆装箱的存在,要小心留意i为null。
【小题】
Integer i1 =200; Integer i2 =200; System.out.println("i1==i2: "+(i1==i2)); Integer i3 =100; Integer i4 =100; System.out.println("i3==i4: "+(i3==i4));
【答案】运行结果为false,true.
首先,==和equals()的区别:
- ==比较的是两个对象的引用是否相同,或者是比较原始数据类型是否相等;
- equals()比较的是两个对象的内容是否相同,可以通过重写equals添加自己的比较逻辑。
其次,-128~127的Integer值可以从缓存中取得,即Integer类型的常用数字缓存IntegerCache.cache。其他情况要重新创建。
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
关于基本数据类型的几点补充
-
short s1=1; s1=s1+1;
这一句编译错误,因为执行s1+1返回的结果是int类型(执行隐式类型转换)。修改的话要强制转换为short型才可以。
short s1=1; s1+=4;
这一句没有任何问题。
-
switch语句不能作用于浮点数类型中,可以作用于char, byte, short, int, Character, Byte, Short, Integer, String or an enum.
-
封装类型+String都是final的,不可被扩展,数字类型的公共父类是Number类,都实现了Comparable接口。
【小题】
public static void main(String[] args) throws Throwable { int j=0; for(int i=0;i<1000;i++) { j=j++; } System.out.println(j); }
【答案】运行结果为0。
解释一(未十分确信):Java使用中间缓存变量机制,j=j++语句会执行为:
temp=j; j=j+1; j=temp;
解释二(靠谱):使用javap反汇编命令进行反汇编,其中j=j++对应的结果是(j对应的变量编号是1):
11: iload_1 //将局部变量j的值放到栈顶:0 12: iinc 1, 1 //将局部变量j的值加1,j=1 15: istore_1 //将栈顶的值放到局部变量j中,j=0
所以从底层实现看,j=j++这一句中的自增操作只是对局部变量的操作,局部变量变化后没有存储到栈顶,反而被之前栈顶的值覆盖了,所以相当于不起作用。
-
数组
-
3种创建方式
int[] arr1 = {1,2,3,4}; //正确 int[] arr2 = new int[4]; //正确 int[] arr3 = new int[]{1,2,3,4}; //正确 int[] arr4 = new int[4]{1,2,3,4};s //错误,编译不通过
-
数组越界,抛出ArrayIndexOutOfBoundsException
-
数组具有length属性
-
如果不对数组指定初值,默认初始化为相应数据类型的默认值。
-
多维数组,嵌套中括号即可。
-
数组是一种特殊的结构,在数组对象的对象头中需要记录数组长度的信息。JVM中的对象包括三部分,即对象头(Mark Word)、实例数据和对齐填充;而对象头(Mark Word)中又分为三部分,包括运行时信息(gc信息,锁标志位等)、类型指针、如果是数组还需要记录长度。
-
-
String
-
不属于基本类型,内部实际值是一个char[]数组
JDK1.6之前,方法区包括运行时常量池在永久代中;
JDK1.7,方法区和运行时常量池在永久代,字符串常量池在堆中;
JDK1.8,永久代被废弃,方法区在元空间,运行时常量池和字符串常量池在堆中。原文 -
创建
String s1="ss"; //先将"ss"存储在池中,然后返回引用 String s2=new String("ss"); //创建新对象,使用的"ss"已经在池中 String s3="Prog"+"gramming"; //创建3个对象,均存储在池中
-
字符串常量池和不可变(Immutable)字符串
字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价。JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池,每当代码创建字符串常量时,JVM会首先检查字符串常量池。如果字符串已经存在池中,就返回池中的实例引用。如果字符串不在池中,就会实例化一个字符串并放到池中。Java能够进行这样的优化是因为字符串是不可变的,可以不用担心数据冲突进行共享。原文
查看源码可以发现,String的定义是
public final class String implements java.io.Serializable, Comparable<String>, CharSequence
,经过final修饰,无法被继承。
扩展点:封装类型Short, Integer均被final修饰,继承自Number类。
扩展点2: 封装类型Short, Integer等也是不可变类型,内部实际值是对应基本类型的名为value
的final变量。【小题】
String str1 ="abc"; String str2 ="abc"; System.out.println(str2==str1); System.out.println(str2.equals(str1)); String str3 =new String("abc"); String str4 =new String("abc"); System.out.println(str3==str4); System.out.println(str3.equals(str4));
【答案】结果是true,true, false, true.前两个String对象从String池中获取,后两个对象是新创建的,内容相同但引用不同。
【小题】
String d ="2"; String e ="23"; e = e.substring(0, 1); System.out.println(e.equals(d)); System.out.println(e==d);
【答案】结果是true,false.因为substring方法的实现会重新创建String对象。
public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > value.length) { throw new StringIndexOutOfBoundsException(endIndex); } int subLen = endIndex - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen); }
【小题】
String str1 = "str"; String str2 = "ing"; String str3 = "str" + "ing"; String str4 = str1 + str2; System.out.println(str3 == str4); String str5 = "string"; System.out.println(str3 == str5);
【答案】结果是false,true。后一个结果应该很好理解,因为第3行代码执行时已经将"string"存储在常量池中,第6行代码返回的是常量池中同一字符串"string"的引用,所以结果为true。前一个结果可能有点疑惑,后来把这段代码编译反汇编后发现执行第3行代码时是直接生成的String对象,内容为"string";而执行第四行代码时是借助new StringBuilder().append(“str”).append(“ing”).toString(),关键在于StringBuilder中定义的toString()方法,返回的是一个重新创建的String对象,并没有存在池中,所以前一个结果为false。
//StringBuilder.toString()源代码 public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
-
由字符串解析为基本数据类型,基本数据类型封装类的parse方法可能会报NumberFormatException,比如
Integer.parseInt("era");
扩展点:Integer a=Integer.parseInt(""); //NumberFormatException JSONObject jsonObject=new JSONObject(); jsonObject.put("num", ""); Integer b = jsonObject.getInteger("num"); //null int bV = jsonObject.getIntValue("num"); //0
-
intern()方法:若由equals()判断池中包含相同的对象则返回池中该对象的引用,否则将新对象加入池中并返回引用。注意不管是怎么创建的,都先从池里找。
-
对于字符串拼接,考虑性能,如果循环进行拼接操作,生成的字符串都会存在池里,并且每次拼接都要重新构造StringBuilder对象,影响性能。因此可以使用StringBuilder优化:
StringBuilder builder...append(String.valueOf("fsdasf"));
-
StringBuilder为什么性能高:StringBuilder内部有一个char数组,初始容量16,每次扩容old*2+2,会先和需要的长度比较,如果不够那么直接扩容到所需的长度。拼接字符串不会有中间结果,因为直接拼接在数组里了,但同时可能造成空间浪费。注意:java中的加号拼字符串已经被jvm优化为StringBuilder来实现。仅当多次拼接(例如循环)时StringBuilder效率更高,否则效率是差不多的。不过这种说法也不完全正确,参见一个例子:
String s = "hello "; String s2 = s + "world"; String s3 = "hello " + "world"; System.out.println(s2 == "hello world"); System.out.println(s3 == "hello world");
结果是false,true。可见,两个字符串直接拼接有可能还有特殊的优化方式,拼接后的结果和常量池结果相同,而不是生成新的String。
StringBuilder扩容时有个最大值是Integer.MAX_VALUE-8,如果所需容量超过这个值,那么仍然按照用户给定的值来分配空间,但有可能会超过VM的限制(VM limit exceeded)。
-
StringBuilder/StringBuffer
- StringBuilder线程不安全,但效率高;
- StringBuffer线程安全,但效率低。
线程安全:某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成
-
2. java中的关键字
java一共有50个关键字。
abstract | assertassert | booleanboolean | breakbreak | bytebyte |
---|---|---|---|---|
case | catch | char | class | const |
continue | default | do | double | else |
enum | extends | final | finally | float |
for | goto | if | implements | import |
instanceof | int | interface | long | native |
new | package | private | protected | public |
return | strictfp | short | static | super |
switch | synchronized | this | throw | throws |
transient | try | void | volatile | while |
-
final:
修饰变量时,用以定义常量;
修饰方法时,方法不能被重写(Override);(注意:重载是指方法相同的多个函数,重写是指子类重写父类的同名方法)
修饰类时,类不能被继承。
final的内存语义,在final域初始化过程,不允许实例化和引用赋值发生指令重排序,即保证了final的不可变性。
-
try-catch-finally:
异常处理关键字。必须有try,catch/finally有其一即可。
java7中带来的try-with-resources特性,把资源开启写在try括号里,可实现资源自动回收。
-
synchronized
线程同步关键字。两种用法,可以synchronized(object)或者加在方法前。加在普通对象或者实例方法上为对象锁,加在.class对象或者静态方法上为类锁。
这个关键字可以引申出java锁的用法,也是一大块内容(待整理)
synchronized锁是基于monitor对象实现的,编译时会在每一处需要同步的代码块开始和起始处添加monitorenter和monitorexit指令。monitor其中计数器机制,monitorenter会把计数器加1,monitorexit会吧计数器减1;当计数器减到0时,锁就被释放了。
synchronized对同一线程是可重入的,不会出现“自己把自己锁死”;对于不同线程,同一时刻只能由一个线程获得锁并执行。
monitor底层是通过操作系统的mutex lock(互斥锁)实现的。
由于竞争锁的过程中可能引起线程状态的变化,过程中会发生用户态和内核态之间的切换,会对性能有影响。因此synchronized是一个重量级操作,所谓重量级就是指这种重量级锁依赖于底层的mutex,而且切换带来的代价比较大。jdk1.6引入了偏向锁和轻量级锁用来优化这个过程,如加入自旋让线程空跑一段时间。
JVM底层存储对象,对象有一个对象头属性,而对象头包括Mark word、类型指针和数组长度。Mark word中通过锁标志位来存储锁的不同状态。
java锁是分级别的,按低到高为无锁、偏向锁、轻量级锁、重量级锁,锁可以升级但不能降级。
-
instanceof:检测对象是否是一个类的实例,对象可以为null
-
volatile: 禁止指令重排序。
volatile的作用是提供轻量级的同步机制,可以保证有序性、可见性,但不保证原子性。
从JMM层面来讲,主内存和工作内存之间的动作可以归结为lock、unlock、read、load、use、assign、store、write。volatile关键字保证了read/load/use和assign/store/write分别必须是连续的,即读取变量必须从主内存刷新,写入变量必须同步到主内存中,这样使得修改的值立即可以被其他线程看见,也就保证了可见性。而有序性是禁止指令重排优化。不保证原子性,是因为虽然保证了读取和写入两个步骤的连续性,但并不保证读取和写入也是连续的。
volatile可以用来增强单例模式的double-check线程安全问题:避免初始化对象和赋值引用两条指令的重排。
-
default: java8引入的,用于在接口中声明默认方法
-
transient: 作用于序列化过程。任何实现Serializable接口的类都可以被序列化,但如果对象中的字段被transient修饰则不参与序列化过程,只存在于调用者的内存中。
被transient修饰的静态变量,其值并不会受序列化和反序列化过程干扰,仅以内存中的值为准。
序列化的两个接口:Serializable不需要实现任何方法,自动序列化;Externalizable,需要实现writeExternal方法指定序列化字段。
-
enum:用来定义枚举类型。
枚举类是个比较特殊的类型,由于在启动时加载而且仅加载一次,很安全的单例模式。
jdk提供的EnumSet, EnumMap通过位向量来压缩存储空间。例如EnumSet的实现有RegularEnumSet和JumboEnumSet,元素少于64个使用RegularEnumSet,内部用一个long变量作为位向量;多于64个则使用JumboEnumSet,内部用一个long数组作为位向量。
-
for:循环关键字。如果使用for-each语法需要注意空指针异常。
3. 集合
-
List, Queue, Set, Map,都是接口而非具体实现。Collection接口和Map平级;List, Queue, Set继承自Collection接口;HashMap继承自AbstractMap;ArrayList, LinkedList, Vector继承自AbstractList;LinkedList还实现了Deque;Stack继承自Vector。
-
List
-
特点是有序并且按线性方式存储,可以根据元素的下标进行精准的控制。
-
ArrayList是顺序表的体现,底层用数组实现。
数组在线性表中的分类属于顺序表,即:顺序表中的数据元素存储是连续的,内存划分的区域也是连续的。
ArrayList初始容量为0,默认最小容量是10,内部通过数组实现。当数组容量不够时需要扩容,扩容大小为1.5倍于先前的大小。
-
LinkedList是链表的体现,使用双向链表实现。(链表的三种形式:单向链表、双向链表、循环链表)
链表在物理存储上通常是非连续、非顺序的方式存储的,但是数据元素的逻辑顺序是连续的,实现方式是通过链表中的引用来实现。
-
Vector通过synchronized实现了线程安全。初始容量为10,内部同样是通过数组实现,而扩容机制不同。如果指定了capacityIncrement参数则每次扩容按照oldCapacity+capacityIncrement扩大,否则直接按照2倍扩容。
-
Stack是Vector的子类,而Vector实现了
List<E>
接口;是栈结构的代表。Stack继承自Vector,自然也可以保证线程安全。其内部push,pop方法都使用synchronized进行同步。push操作正常添加元素,push操作弹出最后面的元素。
栈和队列是特殊的线性表,或者说是受到限制的线性表,其限制是仅允许在线性表的尾部进行添加和删除操作,这一端被称为栈顶,另一端称为栈底。
-
RandomAccess是一个标志接口,表示该类型实现了随机访问,例如ArrayList/Vector
-
-
Queue
-
队列Queue直接继承自Collection接口,是队列结构的代表,使用链表结构实现。
Queue接口是队列的体现,在实现上是基于链表实现的,但是具体的实现类是LinkindList,也就是说,java通过Queue接口收窄了LinkedList的访问权限,只提供从队尾,队头等的操作,从而实现了对列。
(注:Queue继承自Collection接口,而LinkedList实现了Deque接口,Deque接口继承自Queue接口,即实现了Queue接口的所有方法)
-
Queue接口的主要方法:add, offer(添加元素), poll(返回并删除队列头部元素)。
根据offer()方法的官方注解,更加推荐使用offer()方法:
/** * Inserts the specified element into this queue if it is possible to do * so immediately without violating capacity restrictions. * When using a capacity-restricted queue, this method is generally * preferable to {@link #add}, which can fail to insert an element only * by throwing an exception. * ... * @return {@code true} if the element was added to this queue, else * {@code false} * ... **/
-
-
Map
-
Map单独为一个接口,HashMap是基于哈希表的对Map接口的实现,而哈希表的底层数据结构是数组和链表;在java8中又引进了红黑树来降低查询的时间复杂度到O(logN)。
-
特点是能够根据key快速查找value;键必须唯一,put一个键相同的值时该键会被更新,即同一个键只能映射到一个值。
-
键值对以Entry类型的对象实例存在
-
HashMap和HashTable的区别:
- HashMap继承自AbstractMap,HashTable继承自Dictionary
- HashMap允许一个key为null,多个value为null;HashTable不允许key或value为null
- HashMap是线程不安全的,效率更高;Hashtable是线程安全的
- ConcurrentHashMap使用分段锁,也能保证线程安全,比HashTable效率更高
-
HashMap
初始容量是16,负载因子0.75。
HashMap内部是通过数组+链表的形式存储数据,1.8当链表长度大于8时会进化为红黑树,小于6时会退化为链表。红黑树是一种平衡二叉树,能够保证查询的时间复杂度在log(N)。
负载因子是指存储的键值对数和容量的比值。因此有一个阈值(threshold)的概念,阈值=容量×负载因子。当存储一个键值对时,键值对的数量已经达到阈值,而且根据hash值判断对应的数组位置上也已经有值,就会执行扩容操作。
HashMap通过计算key的hash值来判断要将键值对存储在数组的哪个位置上,通过拉链法解决hash碰撞。(常见hash碰撞的解决办法:开放地址法、再哈希、拉链法)
jdk1.7 HashMap:key如果为null默认hash值为0。如果判断数组对应位置上没有键值对就直接插入;否则有可能已经有相同的key了,通过e.hashhash(key)&&(e.keykey||e.key.equals(key)来判断,如果是同一个key那么替换对应Entry的值;否则说明不存在相同的key,使用头插法插入到链表中,这会导致扩容时出现并发问题。当键值对数量达到阈值而且对应数组位置上有值,就会执行扩容。扩容操作会将容量乘以2倍(容量永远是2的幂次),然后分配一个新的数组,遍历原来的老数组中的每一个节点,重新计算在新数组中的位置,也使用头插法插入到新数组对应位置的链表中,然后将新数组赋值给内部数组的引用。然后再进行插入键值对的操作。
jdk1.8 HashMap:同样,null key的hash值为0;如果数组对应位置没有值就直接插入;否则通过e.hashhash(key)&&(e.keykey||e.key.equals(key)来判断如果是同一个key那么替换对应的值;如果这里是链表结构,采用尾插法插入数据,否则为红黑树结构,向红黑树插入结点。如果在链表插入数据后达到了红黑树阈值,那么就把链表转化为红黑树。然后将数组size增加,判断是否已经达到阈值,如果达到了那么进行扩容操作。**这里可以看到1.7和1.8的不同:除了头插法和尾插法的区别,还有1.8是先增加键值对再扩容,1.7恰恰相反。**扩容时,如果当前是一条链表,那么通过(e.hash & oldCap) == 0来判断这个节点是要放在同一个位置,还是这个位置+oldCap上。使用两条链表lo和hi来表示放在原位置和由于扩容放在新位置上的链表,然后在执行插入,这里都使用尾插法,因此不会出现1.7中死循环的问题。
【面试题】HashMap 1.7和1.8的实现有什么不同:
【解答】 (1) 存储结构不同,1.7是数组+链表,1.8是数组+链表+红黑树;(2) 扩容转移数据的方式不同,1.7采用头插法,1.8是尾插法,而且达到转化红黑树的阈值要转化为红黑树;(3) 扩容计算元素存储位置的方式不同,1.7是直接用hash&(newCap-1)计算,1.8是通过hash&oldCap==0来判断,1.7是一个一个键值对移动,1.8是先排列成两个链表然后直接把链表转移过去;(4) 扩容和增加键值对顺序不同,1.7是先扩容后增加键值对,1.8是先增加键值对后扩容。
【面试题】HashMap的容量为什么是2的幂次?
【解答】当容量是2的幂次时,n-1对应的掩码是连续的1,可以保证数据均匀分布。
-
LinkedHashMap
继承自HashMap结构。其内部保存了节点Entry的双端链表结构,通过重写HashMap的一些方法来增加对链表的处理。有序性就是基于插入和删除时操作的顺序都被记录在链表中。
jdk1.7中,只有一个header作为链表头,初始化为空Entry。
伪代码:
addBefore(LinkedHashMap.Entry e) { this.before = e.before; this.after = e; this.before.after = this; this.after.before = this; }
jdk1.8中,head, tail属性保存链表,初始化为null。实现相对复杂。
-
ConcurrentHashMap
ConcurrentHashMap实现了线程安全。通过分段锁来同时兼顾处理效率,默认并行度为16,即允许16个线程并发读写。其内部结构也是数组+链表,jdk1.8也有变成红黑树的操作。内部有一个分段锁数组,每一个分段锁对应一部分数据,这样需要读取时只会锁住这一部分数据,而不会影响其他数据的读写。
-
-
Set
-
直接继承自Collection接口,HashSet内部使用HashMap实现;
-
特点是无序但是不包含重复的元素,可以用来去重
-
元素存放方式为散列存放,即通过计算hash值存放元素。
-
对象的相等性通过hashCode/equals判定
-
HashSet
其内部通过HashMap来实现。原理是元素作为key,value用一个PRESENT常量代替,即通过HashMap的key唯一性来保证HashSet不存在重复元素。因此HashSet判断元素相等性和HashMap的判断方法一致,即hash相等&&(地址相等或equals相等)。
HashSet.add方法伪代码:
add(E e) { return map.put(e, PRESENT) == null; }
-
4. 面向对象
-
面向对象三大特征:封装、继承和多态
-
构造函数:创建对象时调用。若未显式定义构造函数,系统自动生成无参构造函数。
-
重载:发生在一个类中,函数名相同,参数列表不同(类型/个数)
-
重写:发生在两个类中,函数名相同,参数列表相同。
-
继承:初始化子类时先初始化父类,即调用构造函数时隐式执行父类构造函数。
-
单继承性:Java允许一个类只能有一个父类。
-
super关键字:既可以作为父类对象的引用调用父类方法,也可以作为父类构造函数名显式调用父类构造函数。
-
垃圾回收:对象被回收时会调用finalize()方法。
垃圾回收机制:当垃圾回收器(Garbage Collector)认定对象没有任何引用时会将其回收,在回收前调用finalize方法。但是《Java编程思想》中还提到了这样一句话:
记住,无论是“垃圾回收”还是“终结”,都不保证一定会发生。如果Java虚拟机(JVM)并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。
看起来Java的垃圾清理机制似乎并不是那么完美。调用System.gc()方法可以强制进行垃圾回收。
-
多态:
Employee e=new Manager();
运行期间JVM动态推定对象的实际类型为Manager。
-
封装:
访问修饰权限:private, default, public, protected
修饰符 当前类 同一包内 子孙类 其他包 其他包子孙类 public
Y Y Y Y Y protected
Y Y Y N Y/N(说明) default
Y Y N N N private
Y N N N N protected关键字特别说明:
- 子类与基类在同一包中:被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问;
- 子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的protected方法。
-
抽象类不能被实例化,实例化由子类完成;但是抽象类也是类,可以有构造方法!
-
抽象类不一定要有抽象方法;抽象方法所在的类一定是抽象类;
-
abstract和final不能同时修饰一个类,否则编译不通过。因为final声明的类不能被继承,如果同时修饰意味着这个类永远是抽象类。
-
abstract不能与private, static, final, native的同时修饰一个方法,否则编译不通过。
-
模板方法模式
抽象类体现的就是一种模板设计模式,抽象类作为多个子类通用的模板,子类在抽象类的基础上进行扩展,这种模式解决的就是一部分功能不确定,就把不确定的功能部分暴露出去,让子类自己去实现。
-
接口是更高级别的抽象,接口中的方法必须都是抽象方法;接口中声明的方法都是没有方法主体的;
-
接口中的属性默认修饰符public static final,方法默认修饰符public abstract,这些都可以省略。
-
一个类可以实现多个接口,但只能继承一个父类。
扩展点:接口和抽象类的主要区别
-
抽象类是一种对事物的抽象,而接口是一种对行为的抽象;
-
抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
-
抽象类是一种模板式设计,而接口是一种行为规范,是一种辐射式设计。
模板式设计的含义是:如果B和C都使用了公共模板A,如果他们的公共部分需要改动,那么只改动A就可以了;
辐射式设计的含义是:如果B和C都实现了公共接口A,如果现在要向A中添加新的方法,那么B和C都必须进行相应改动。
-
-
父类和子类的初始化顺序:父类静态块-子类静态块-父类构造块-父类构造方法-子类构造块-子类构造方法
-
匿名内部类:继承自抽象类或接口的不具有类名的内部类。例如Runnable是一个抽象接口,内部只有一个run方法。
Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println("THread"); } }); thread.start();
扩展点:jdk1.8引入的lambda表达式,只有一个方法的接口情形下,可以使用lambda表达式简化代码。
Thread thread = new Thread(()->System.out.println("Thread"));
扩展点2:查阅源码可发现Runnable上带有注解@FuntionalInterface,该注解自jdk1.8引入,表示是一个函数,只有一个抽象方法;编译器会在编译期检查函数定义。@FuntionalInterface可以注解在类,接口或枚举类型上。
扩展点3:fastjson 中的TypeReference原理是匿名内部类+反射动态获取泛型信息。
扩展点4:匿名内部类使用外部参数时,编译器强制要求参数为final不可变参数。内部类底层实现是拷贝外部形参的引用,然后在内部类调用时是使用拷贝的那一份引用,这样导致外部形参有修改的可能。但是从程序角度来看这是不可接受的(本来就是一个参数,为什么我外面改了内部类还没改这不是很奇怪吗),因此使用final可以避免这种问题,保证了外部形参和内部类的参数是一致的。
5. 异常处理
-
Error和Exception, checked异常和unchecked异常
-
Error(unchecked异常)
是程序无法处理的错误,通常发生于虚拟机自身,表示运行应用程序中较严重问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
-
Exception(checked异常)
必须处理的异常:Checked异常是Java特有的,在java设计哲学中,Checked异常被认为是可以被处理或者修复的异常,所以Java程序必须显式处理Checked异常,当我们使用或者出现Checked异常类的时候,程序中要么显式try- catch捕获该异常并修复,要么显式声明抛出该异常,否则程序无法通过编译。
(注:以上内容引用他人文章,原作者声明:作者:胖先森链接:https://juejin.im/post/5a81941ff265da4e976e9004来源:掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。)
-
-
try-catch-finally
可以用多个catch子句处理不同异常,级别由低到高。
try { doSomethint(); } catch (Exception1 e) { doSomethingCatch1(); } catch (Exception2 e) { doSomethingCatch2(); } finally { finallyDoSomething(); }
扩展点:在Java SE 7或者更高版本中,一个catch块可以同时处理多种异常类型,有助于减少重复代码。
//... } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); }
注意当catch语句要捕获多种异常类型时, 参数(在这里是e)隐式地成为final变量。
扩展点2:try子句后,catch和finally不是必须得,有一个出现就可以。 -
方法声明异常的关键字是throws,抛出异常throw。
-
try-with-resources子句(Java SE 7引入)
援引官网的介绍吧:
The
try
-with-resources statement is atry
statement that declares one or more resources. A resource is an object that must be closed after the program is finished with it. Thetry
-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implementsjava.lang.AutoCloseable
, which includes all objects which implementjava.io.Closeable
, can be used as a resource.
6. 多线程和并发
-
线程是操作系统调度的最小单元,也叫轻量级进程。同一进程可以创建多个线程,而他们都拥有各自的计算器、堆栈和局部变量等属性,并且能够访问共享的内存变量。
-
线程的5个状态
创建(New):使用new关键字创建一个线程
就绪(Ready):调用start方法,等待CPU调度
运行(Running):执行run方法
阻塞(Blocked):由于一些原因放弃CPU使用权,暂时停止执行
死亡(Dead):run方法执行完毕或者执行时产生异常
-
Java中线程的状态
Java中自己定义了六种状态,与操作系统定义略有不同。
- NEW:新创建的线程
- TERMINATED:结束运行的线程
- RUNNABLE:资源已就绪,等待cpu分配时间片即可执行
- WAITING:Object.wait()(不指定时间)或Condition.await(不指定时间),线程处于等待队列中
- TIMED_WAITING:Object.wait() (指定时间)或Condition.await(指定时间),线程处于等待队列而且设置了计时器
- BLOCKED:IO操作造成阻塞,或者synchronized或lock.lock,线程在竞争队列中,尝试获取锁但没有获取成功时阻塞
线程进入阻塞态的方式:(1) IO操作 (2) 同步代码还没有获取到锁 (3) Thread.sleep (4) Thread.join
-
几个重要的名词区分
-
同步和异步
同步方法调用后必须等待方法返回才能执行后续行为;异步方法调用后可以立刻执行后续行为。
在I/O模型中,同步异步指的是用户进程读取数据和内核处理由网卡传送过来的数据这两个过程之间是同步还是异步,如果内核没处理好数据时用户进程阻塞则为同步,否则为异步。
-
并发和并行
并行是真正意义上的多个任务同时执行;并发是支持处理多个任务,不一定要同时,多个任务可能是串行的,但每个任务只能获取CPU很短的占用时间,多个任务在很短的时间内交替执行。
我相信你已经能够得出结论——“并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。
-
阻塞和非阻塞
阻塞是指某一线程访问一公共资源时其他线程必须等待该线程释放资源才可以使用,否则就要挂起线程等待;非阻塞是指线程之间不会发生资源争夺。
-
原子性
原子性是指一个操作是不可被中断的,即使多个线程是同时执行的。
-
可见性
可见性是指当某个线程修改了共享变量的值,其他线程能否立刻知晓。
-
有序性
**Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。**前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存和主内存同步延迟”现象。
-
-
volatile和synchronized
Java中提供了关键字volatile和synchronized关键字来保证线程之间操作的有序性。volatile包含了禁止指令重排序的语义,并保证不同线程对同一变量操作时的可见性;synchronized关键字对同一时刻同一变量只允许一个线程对其进行lock操作。
volatile保证可见性、有序性,不保证原子性。 -
线程创建
- 继承Thread类重写run()方法
- 匿名内部类,即在new Thread()后跟具体的定义体,其中重写了run()方法
- 实现Runnable接口,重写run()方法
-
开启线程的run()和start()方法区别
- run()方法不能新建一个线程,而是在当前线程中调用run()方法;
- start()方法新建一个线程并调用其run()方法。
-
终止线程不要使用stop()
一般情况下,线程在执行完毕后就会结束,无需手工关闭,但是我们也经常会创建无限循环的后台进程以持续提供某项服务,所以就需要手动关闭这些线程。
在JDK中也有终止线程的API,例如stop()方法,但是极度不推荐这个方法,因为stop()方法得到调用后,会强行把执行到一半的线程终止,可能会引起数据不一致问题。但是想要终止一个无限循环的线程应该怎么做?
我们推荐的做法是在类中添加一个isStop的布尔值属性,判断isStop为true则跳出循环体,线程执行完毕自动终止,就避免了数据不一致的问题。
-
wait() 和notify()
这两个方法不是Thread类特有的,而是所有类的父类Object中的。
当一个对象调用了wait方法后,如:objectA.wait(),当前线程就会在这个对象上等待,会释放该对象的锁,直到其他线程调用了objectA.notify()方法为止。
需要注意的是,wait和notify方法都必须获得对象的监视器(锁),在同步代码得到执行后也会释放对象的锁,所以必须被包含在对象的synchronzied语句中。(注:以上内容援引他人文章,原作者声明:作者:kk_miles链接:https://juejin.im/post/5a30a7466fb9a0450f21ec5c来源:掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。)
-
小题
public class WaitNotifyDemo { final static Object object = new Object(); public static class Thread1 extends Thread{ @Override public void run() { synchronized (object){ System.out.println(System.currentTimeMillis()+" 线程1开启。。"); try { object.wait(); //1.先获取object的锁,然后开始等待,并再次释放object的锁。 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(System.currentTimeMillis()+" 线程1结束。。"); //4f. 两秒后,线程2执行结束,线程结束,重新获得了object的锁,此句猜得到执行 } } } public static class Thread2 extends Thread{ @Override public void run() { synchronized (object){ System.out.println(System.currentTimeMillis()+" 线程2开启,并执行notify通知线程1 。。"); object.notify(); //2.获取object的锁成功,通知object的其他线程(即线程1),这里还未释放object的锁! System.out.println(System.currentTimeMillis()+" 线程2执行notify结束。。"); try { Thread.sleep(2000); //3. 使线程2暂停2秒,即2秒后线程2才能执行结束,才能把object的锁释放。 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(System.currentTimeMillis()+" 线程2结束。。"); } } } public static void main(String[] args) { Thread thread1 = new Thread1(); Thread thread2 = new Thread2(); thread1.start(); thread2.start(); } }
以上代码是一个关于wait和notify的示意代码,声明一个静态对象和两个静态内部类表示两个线程,连个线程分别在对象object的同步块中调用wait和notify方法,在线程1中调用了object的wait方法前需要先获取object的锁,然后进入线程等待,并释放锁;然后线程2的notify方法执行前需要获取object的锁,然后通知线程1。但是此时线程1仍然无法执行wait方法后面的代码,原因是线程2Thread.sleep(2000)使得线程2在2秒之后才能退出并且释放object的锁,也即线程1必须等待线程2的object同步代码块执行结束后才能获得锁,去继续执行下面的代码,具体的输出日志如下:
1510300350884 线程1开启。。 1510300350884 线程2开启,并执行notify通知线程1 。。 1510300350884 线程2执行notify结束。。 1510300352884 线程2结束。。 //2秒中之后线程2结束,释放object锁 1510300352885 线程1结束。。 //线程2释放锁之后,线程1获得锁,结束等待状态,继续向下执行。
-
synchronized原理(待整理)
通常认为synchronized是通过monitorenter, monitorexit指令来实现同步,monitor是对象上的一个监视器,底层通过操作系统的互斥锁(mutex)来实现,这样就是一个重量级操作,因为要发生内核态和用户态的切换。
实际上jdk1.6已经将synchronized优化,锁的等级由小到大为无锁、偏向锁、轻量级锁、重量级锁,不一定每次都是重量级操作,因此不必过于担心synchronized的性能问题。
一般从程序执行上只可能有一个线程执行同步代码的,JVM会直接处理为无锁执行。
默认情况下采用偏向锁的方式。即线程进入同步代码块时,检查对象头是否是无锁状态,如果是无锁状态则将其置为偏向状态并将线程id置为自己的id;如果是偏向锁状态则通过CAS操作竞争锁,如果竞争没有成功,则在到达全局安全点时,JVM检查持有锁的线程是否存活,如果不存活则将其重置为无锁状态,然后偏向新的线程;否则说明有线程竞争,则升级为轻量级锁,竞争的线程进入自旋等待。偏向锁不会主动释放。
升级为轻量级锁时,线程栈帧中会建立一块Lock Record的空间,来存储锁对象的Mark word的拷贝,然后通过CAS操作将对象的Mark word指向栈帧中的Displaced Mark Word,如果成功则加锁成功。否则说明有竞争,可以通过自旋操作稍微等待一下,但如果超过了自旋限制次数,或者又来了第三个线程,那么将升级为重量级锁,将会导致线程实际阻塞。
重量级锁是依靠对象中的监视器锁(monitor)实现的,监视器又是依靠底层的互斥锁(mutex)实现的,此过程中发生了用户态到内核态的转变,如果竞争不成功会导致线程进入阻塞状态。
-
JUC包中的lock(待整理)
JUC包提供了一系列lock,如ReentrantLock,ReentrantReadWriteLock。和synchronized的区别:synchronized由jvm自动处理加锁解锁操作;lock则有程序猿自己控制。lock的操作有lock(), tryLock(), tryLock(timeout, timeunit)。lock是阻塞操作;tryLock是非阻塞操作;tryLock是阻塞等待一段时间,超时则停止等待。
lock提供了类似于Object.wait/notify的机制,即Condition.await/signal。其区别在于lock可以有多个Condition,即有多个等待队列和一个同步队列。
ReentrantReadWriteLock可重入读写锁,类似于MySQL的共享锁和排它锁,读操作为共享锁,写操作为排它锁;共享锁和共享锁可以共存,排它锁不能和排它锁或共享锁共存:即同一时刻可能有多个读操作或一个写操作,不会有读写同时进行的情况。
-
线程池(待整理)
其实主要是几个核心参数:coreSize, queue, maxSize, rejectPolicy, keepAliveTime。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
- corePoolSize:核心线程池数量
- workQueue:等待队列。包括:
- ArrayBlockingQueue:基于数组的有界阻塞队列,FIFO
- LinkedBlockingQueue: 基于链表的无界阻塞队列,FIFO
- SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等待其他线程调用移除操作,否则将会阻塞
- PriorityBlockingQueue:具有优先级的阻塞队列
- maximumPoolSize:最大线程池数量
- keepAliveTime:核心线程池以外的线程,经过keepAliveTime时间空闲状态后将会被回收。
- rejectPolicy:拒绝策略,如果线程池已不能接受新任务提交时的拒绝策略。包括:
- AbortPolicy: 抛出RejectedExecutionException
- CallersRunPolicy: 由当前线程来执行
- DiscardPolicy:抛弃策略,直接丢弃任务
- DiscardOldestPolicy: 丢弃任务队列中最老的任务,然后尝试执行
7. I/O
-
流是一组有顺序的, 有起点和终点的字节集合,是对数据传输的总称或抽象。
-
I/O流的分类
- 根据处理数据类型划分为:字符流和字节流
- 根据数据流向划分为:输入流和输出流
- 根据IO模型划分为:传统IO(BIO)和新IO(NIO, New I/O)
-
字符流和字节流
字符流本质上就是基于字节流,读取时查询相应的码表。
区别:
-
读写单位:字节流以字节为单位;字符流以字符为单位,一次可能读多个字节;
-
处理对象:字节流能处理所有类型的数据(图片、视频等),而字符流只能处理字符类型的数据。
(文本数据优先考虑字符流,其他情况都用字节流)
-
-
Java 基本I/O操作可以参考这篇文章
-
基本IO模型可分为阻塞IO、非阻塞IO、IO多路复用、信号驱动IO和异步IO。传统的IO模型是阻塞IO模型,通常一个线程对应一个IO操作,如果IO没有完成则阻塞。java中的NIO是多路复用模型,并不是非阻塞模型。非阻塞由于其自身缺点,一般不会直接使用,只是把非阻塞的思路应用到某一种IO模型上。异步IO由于Linux没有很好地实现,所以广泛使用的还是IO多路复用技术,如select, poll, epoll。
-
netty(待补充)
8. 网络编程
-
Java建立TCP连接的步骤:
(1) 服务器实例化一个ServerSocket对象,通过服务器的特定端口通信;
(2) 服务器调用ServerSocket的accept()方法,一直等待直到客户端连接到服务器的端口为止;
(3) 服务器等待时,客户端实例化一个Socket对象,指定服务器地址和端口号请求连接;
(4) 客户端的Socket构造函数尝试连接到服务器指定端口,如果成功连接,在客户端创建一个Socket对象使得可以与服务器通信;
(5) 服务器端accept()方法返回一个新的socket引用,使得可以连接到客户端。
-
Spring中实现WebSocket可以直接用注解方式,类上@ServerEndPoint,然后实现@OnOpen,@OnMessage,@OnCLose,@OnError方法。在@OnMessage中可以插入心跳机制。
-
网络部分的内容还是比较多的,涉及计算机基础理论,可移步专栏计算机网络_程序员小辰的代码小窝-CSDN博客
9. 反射
-
Java的反射机制是指程序可以访问、检测并修改本身的状态或行为的一种能力,并能根据自身行为的状态和结果调整或修改应用所描述行为的状态和相关的语义。
通过反射机制,可以访问Java对象的属性、方法、构造方法、关键字、返回值等等。
-
实例操作:
获取Class对象
Class c = Class.forName("A");
Class c = A.class;
Class c = new A().getClass();
利用Class对象获取新实例
Object o = c.newInstance();
进行相关操作,比如获取属性、方法等等,例如
Field fs = c.getDeclaredFields()[0];
-
【面试题】java创建一个对象的4种方式?
【解答】 (1) new (2) 反射 (3) 反序列化 (4) clone
10. 注解
注解现在用到的地方也比较多了,Spring Boot中大量应用注解,改变了传统基于xml方式配置的代码风格,让配置可以通过代码完成。其原理也是通过反射获取类或者属性或者方法上的注解。
注解部分的内容可以参考Java中的注解
关于Java基础的内容还是挺多,大厂面试也会问的比较细,本文会阶段性更新内容,欢迎大家交流讨论。
参考链接: