写在前面:JAVA神书:
《java编程思想》(Thinking in Java)
《java核心技术》
Spring是业务层框架
WEB层 业务层 持久层
SpringMvc Spring SpringJDBC
这些都是Spring的一部分,Spring一站式开发,他在所有层面都给你提供了解决方案。
IOC Di 为了解耦
实现过程:
使用者 (工厂 XML配置文件) 资源
AOP
jar.exe 打jar包:day11 07
- maven clean
- maven compile
- maven test
- maven package
- maven install
- maven deploy
Java语言基础
基本类型
JAVA中8中基本数据类型
1 | byte short int long float double char boolean |
默认类型和转换问题
1 | /** |
位逻辑运算符
计算时,先把十进制数字转换为二进制数字
与(& and)、或(| or)、非(~)、 异或(^)
详情:https://www.cnblogs.com/lichengze/p/5713409.html
1 | package Test; |
逻辑运算符
&& || (短路与、短路或)
位移运算符
计算2*8最快的方式:8<<1
1 | int i = 8; |
无符号位移
无符号右移:>>>
无符号左移: <<<
即无论正负数,右移之后符号位均补 0
计算-2 >>> 1结果是多少
1 | -2 的二进制求法是正数取反加1。 |
关键字
transient
修饰的变量不参与序列化。例:Map中的entrySet变量native
与C++联合开发时使用,使用此关键字说明这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。例如:Object的hashCode()
,getClass()
等。参考:https://www.cnblogs.com/KingIceMou/p/7239668.htmlvolatile
标识修饰的变量随时可能被修改,所修饰变量的值被修改后会立即被写入主存。可见性,有序性,不具备原子性,是一种比sychronized更轻量级别的同步机制。
输出流
1
2 > System.out和System.err,为 标准输出流 和 标准错误输出流
>
out存在缓冲区;err不存在缓冲区,只要有内容立即输出。【然而目前的验证发现并不是这样,err在out上面时,也存在先输出out,之后才输出err的情况】
1 | package IO; |
内部类
参考连接:https://blog.csdn.net/weixin_44929171/article/details/90318941
将类定义在一个类内部,即成为了内部类。内部类产生的class文件名为“外部类$内部类”
成员内部类、局部内部类、匿名内部类、静态内部类。
成员内部类
成员内部类中不能定义static变量和static方法
1 | package JBasic; |
局部内部类
在类的方法中,作为局部的类,不能被任何其他地方引用。就像局部变量一样,因此不能有任何访问修饰符
1 | package JBasic; |
匿名内部类
类的定义和创建一起完成,目的是创建一个只使用一次的类的实例。作用是为了简化书写
1 | package JBasic; |
静态内部类
与类的其他成员相似,静态内部类使用static修饰,也称嵌套类
1 | package JBasic; |
- 静态内部类中可以定义静态成员,而成员内部类不能;
- 静态内部类只能访问外层类的静态成员,成员内部类可以访问外层类的实例成员和静态成员;
- 创建静态内部类的实例不需要先创建一个外层类的实例;相反,创建成员内部类实例,必须先创建一个外层类的实例。
1 | interface Entry<K,V> { |
原生命令编译java代码
D盘下存在一文件:Hello.java
1 | package com.insist.demo; |
在D盘根路径唤出cmd命令行,执行命令javac Hello.java
,该命令表示用java编译器编译Hello.java
文件,编译成功,什么都不显示。生成字节码文件Hello.class
。
执行命令java Hello
,该命令表示用java虚拟机解释并执行Hello
类,发现出错:错误: 找不到或无法加载主类 Hello。
原因:一个类的全名是包名+类名,所以类Hello的全名是:com.insist.demo.Hello
。
javac命令不会出错是因为javac编译的是文件,存在Hello.java
这个文件,所以不出错。
java命令出错是因为不存在Hello
这个类。
解决方案:在D盘下新建com/insist/demo文件夹(三个文件夹),将Hello.class放到com/insist/demo文件夹下,然后在D盘根路径唤出cmd命令行,执行java com.insist.demo.Hello
即可成功,打印出hello world
。
类、对象的加载过程
main方法所在的类会先被初始化
1 | Father.java: |
类初始化过程:
类被初始化的触发条件:第一次用到该类,就可以说是第一次有new他的对象的时候,会先进行类初始化。
还有就是main方法所在的类会先进行初始化
执行clinit(),该方法由静态类变量显示赋值代码,静态代码块组成,从上到下顺序执行。子类初始化需先初始化父类。只执行一次clinit()。
clinit()是虚拟机帮我们生成的,在类得字节码文件(class文件)中能够看到。classinit
实例初始化过程:
执行init(),init()可能有多个,有几个构造方法就有几个init(),该方法由非静态类变量显示赋值代码,非静态代码块(从上到下顺序执行)和对应构造函数代码(最后执行)组成。init()的首行一定是super()或super(实参列表),即对应父类的init()。每次创建一个对象,都要执行一次init()。
哪些方法不能被重写?
final方法
静态方法
private修饰的子类不可见的方法
Father.java
1 | package hx.insist.com.Demo; |
Son.java
1 | package hx.insist.com.Demo; |
分析:存在main()方法,先对main()所在的类进行初始化,执行Son()的clinit()。Son是Father的子类,所以先执行Father的clinit(),再执行Son的clinit()。
Father的clinit()由静态变量显示赋值代码和静态代码块组成,就是这些:
1 | private static int j = method(); |
执行静态变量j的显示赋值代码时,会执行method(),输出5,此时子类存在同名的静态方法,但是静态方法是不能被子类重写的,所以这个地方还是会执行父类自己的静态方法method()。
然后输出1。
Son的clinit()由静态变量显示赋值代码和静态代码块组成,就是这些:
1 | private static int j = method(); |
和上面一样,执行method(),输出10。然后输出6。
以上就是在发现main()方法时,进行的动作:进行main()方法所在类的初始化(类Son的初始化),然后由于Son是子类,所以要先执行其父类Father的初始化。
接着开始执行代码:Son s1 = new Son();
执行这句代码时要用到类Son,所以要先初始化类,由于之前类Son已经初始化过了,一个类只会被初始化一次,所以不再进行初始化,这里new一个对象,就是进行了对象的初始化(或称实例初始化)。对象初始化会执行对象的init()方法。init()由super(),非静态变量显示赋值代码,非静态代码块和对应构造函数组成,super()就是执行父类的init()。所以先执行Father的super(),非静态变量显示赋值代码,非静态代码块,对应构造函数。然后再执行子类Son的非静态变量显示赋值代码,非静态代码块,对应构造函数。
Father的super()是执行Object类的init(),不会由输出,不管。
Father的非静态变量显示赋值代码,非静态代码块,对应构造函数是这些:
1 | private int i = test(); |
非静态变量显示赋值代码,非静态代码块从上到下顺序执行,对应构造函数一定是最后执行。所以先执行i的赋值语句i = test()
,这句代码会执行test(),但是子类也同时存在一个同名的重写函数test(),又由于父类的test()是public修饰的普通函数,所以test()会被子类覆盖,执行为子类的test()。【也可以这样理解:执行非静态方法时,前面都有一个默认的对象this,执行test(),其实执行的是this.test()。而this代表当前对象,当前正在创建Son对象,所以this代表Son对象,执行this.test()就是执行Son的test()。】这里是不是多态还有待考究,因为多态一定要是基类指针指向子类对象,而这里的this指针到底是代表Father还是Son,我还不清楚,我感觉是Son。如果this指针是指向Son的话,这里就不是多态了,用派生类的指针调用派生类的方法,当然是理所应当的。只有当使用基类指针:指向Father的指针调用test()时,才会产生调用自己的test()还是调用被子类重写的test()这个问题,答案是调用被重写的test(),这样才是态。
综上,会先调用子类的test(),输出9,然后输出3,最后输出2。
接着执行子类的init():
1 | private int i = test(); |
执行i = test()
,调用子类的test()方法,输出9,然后输出8,最后输出7。
所以会先输出:5 1 10 6 9 3 2 9 8 7
然后执行System.out.println();
换行。
最后执行Son s2 = new Son();
。没new一个对象就会进行一次对象初始化(或称实例初始化),所以还会再执行一次上述对象初始化过程,输出:9 3 2 9 8 7
字符串
String、StringBuffer、StringBuilder
三者在执行速度方面的比较:StringBuilder > StringBuffer > String
StringBuilder比StringBuffer块的原因:StringBuilder线程不安全,单线程使用较为合适。
StringBuilder和StringBuffer都比String块的原因:String是字符串常量,其他两个是字符串变量。
StringBuffer多线程安全的原因:在许多方法上添加了synchronized锁
对于三者使用的总结: 1.如果要操作少量的数据 = String
2.单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
3.多线程操作字符串缓冲区 下操作大量数据 = StringBuffer
为什么把String,StringBuffer,StringBuilder都设计称final的,而其他类比如ArrayList,就不是final的?
主要是为了”效率“和”安全性“,如果指定一个类为 final,则该类所有的方法都是 final。java 编译器会寻找内联所有的 final 方法。 此举能够使性能平均提高 50%。(咱也不懂啥意思)
重载与重写,多态
继承一个类,把父类的某个方法重新实现一遍,就叫重写(Override);重写过程中,方法名,参数,返回类型必须和父类保持一致。【重写是父类的方法不能满足现有需求时发生的】
同一个类中,相同的方法名,参数类型不同,或参数个数不同,就叫重载(Overload)。(返回值不同不能作为重载的判断条件)【重载是一个类本身想要满足不同场景时发生的,因为重载本身的参数就不同,因此在硬编码时期就已经可以通过参数判断调用的是哪个方法,所以重载和多态无关】
多态三要素:
1、继承
2、重写(子类重写父类方法)
3、父类指针指向子类对象
集合
总述
Iterable
实现这个接口允许对象成为 “foreach” 语句的目标。 所有单例集合均实现此接口,双利集合Map凭entrySet得到的Set集合是单例的,所以间接的也可以使用foreach。
Collection
抽象类AbstractCollection
实现Collection
抽象类AbstractList
实现List
ArrayList
,LinkedList
,线程不安全
示意图:
-
-
-
ArrayList
ArrayList
内部是用Object[]数组实现的,非线程安全,效率高。便于索引,在不扩容的情况下也便于增插。基本来说是增删慢,查询快。
ArrayList扩容机制:若初始集合list没有指定容量,那么默认内部维护的数组长度为0。则首次进行add()操作时,会扩容为DEFAULT_CAPACITY
,即为10;若指定了容量,则扩容时,将容量增加一半:newCapacity = oldCapacity + (oldCapacity >> 1)
。
这个增加有个细节:如果指定的容量为1,那么执行newCapacity = oldCapacity + (oldCapacity >> 1)
后newCapacity
还是1,无法扩容,所以java增加了保护机制,添加判断
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
,此情况适用于指定容量为1时,1-2<0 所以设置newCapacity
= 2。保证扩容成功。
1 | public boolean add(E e) { |
ArrayList的插入元素:
1 | List list = new ArrayList(); |
原理:先将index后的元素通过拷贝覆盖的方式后移,然后像普通add()一样elementData[index] = element
1 | public void add(int index, E element) { |
ArrayList的清空clear():
1 | list.clear();//for (int i = 0; i < size; i++) elementData[i] = null; |
ArrayList的get():
1 | System.out.println(list.get(3));// return (E) elementData[index]; |
ArrayList的移除元素remove():
1 | list.remove(2); |
其中,重点就是这个System.arraycopy();
1 | /** |
举个例子:
1 | List list = new ArrayList(); |
其实就是
1 | int[] arr = {2,5,1,8,0,0,0,0,0,0};//没有设置容量,第一次add()会将length设置为10 |
Vector
底层结构是数组 Object[] ,线程安全(所有操作在方法上面加上
synchronized
关键字),增删慢,查询快.
如果没有设置初始容量,那么初始容量是10:this(10);
1 | public Vector() { |
操作
1 | Vector v = new Vector(); |
LinkedList
LinkedList内部的数据结构是双向链表,因此没有扩容机制这一说。非线程安全,增快,插、查、删都需要遍历index个元素,所以较慢,效率低。
实现原理:
1 | public class LinkedList<E> |
增加元素add()
:直接增加,改变指针指向即可
1 | //添加元素,保证双向链表为双向的关键操作 |
插入元素add(index,e):由于一般情况下都要遍历index次,所以效率较低
1 | public void add(int index, E element) { |
查找元素:get():要遍历index个元素
1 | public E get(int index) { |
删除元素:remove(index i),remove(Object o):要遍历index个元素
1 | public E remove(int index) { |
比较:
ArrayList在不涉及扩容时,增,查非常快;删,插都要进行System.arraycopy()
,通过拷贝覆盖的方式移动元素,所以效率一般,不是太慢。而在涉及扩容时,增操作要计算容量,再进行拷贝操作elementData = Arrays.copyOf(elementData, newCapacity);
,所以效率稍微会慢。
LinkedList进行增操作时,只需改变指针指向,较快。而在进行插、查、删操作时都要遍历index个元素才能找到进行操作的位置(然后再改变指针指向),所以较慢。
集合的线程安全性
请问 ArrayList、 HashSet、 HashMap 是线程安全的吗?如果不是我想要线程安全的集合怎么办?
查看这些集合的源码可知,每个方法都没有加锁,显然都是线程不安全的。
在集合中 Vector 和 HashTable 倒是线程安全的。你打开源码会发现其实就是把各自核心方法添加上了
synchronized 关键字 。
如果想要线程安全的集合可以使用Collections工具类:可以让上面那 3 个不安全的集合变为安全的。
1 | List<Object> list1 = Collections.synchronizedList(new LinkedList<>()); |
原理:将集合的核心方法添加上了 synchronized 关键字。
1 | static class SynchronizedList<E> |
这里加的锁是mutex
,就是this
1 | final Collection<E> c; // Backing Collection |
HashMap
基于数组+链表的数据结构实现,数组的元素是节点Node,节点中有Node next,可形成链表。
非线程安全,高效,支持Null值和Null键,
HashMap究竟是怎么数组加链表实现的:
1 | public class HashMap<K,V> extends AbstractMap<K,V> |
HashMap对元素储存的方式:先调用key的hashCode(),将得到的值对数组长度取模运算,就可以得到此key在数组中对应的位置。然后看该位置是否有元素,如果没有就直接放入;如果有就调用key.equels()对该元素进行比较,看是否返回true,如果是true,代表两元素相同,则使用原key,新value;若返回false,则代表不同,那么就接着链表后面的元素一直比较,直到链表末尾,如果没有相同的,那么就将此键值对添加到链表末尾。
详情请看我的另一篇详细文章:http://hanhanhanxu.coding.me/2019/06/10/Hash/
为什么HashMap的默认初始容量位16?
为了减少hash碰撞,提高了查询速率,使元素较均匀的放在数组中。
解释:计算索引时HashMap是这样计算的:return h & (length-1);
这里的h是key.hashCode(),简单说就是一个数字;length是HashMap的容量,即16。
1 | length-1 = 15 //15的二进制为00000000 00000000 00000000 00001111 |
为什么HashMap容量都是2的幂次方
因为得到索引时,可以通过按位与(&)操作来计算余数,比求模(%)更快,而且充分散列,减少碰撞。
当容量是2^n时,h & (length -1) == h % length。
扩容机制:
1 | static final float DEFAULT_LOAD_FACTOR = 0.75f; //填充因子 |
Hashmap的resize()包含扩容和ReHash两个步骤,ReHash在并发的情况下可能会形成链表环。
- 扩容
创建一个新的Entry空数组,长度是原数组的2倍。 - ReHash
遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。
为什么HashMap是线程不安全的?
不安全原因:
(1)在put的时候,因为该方法不是同步的,假如有两个线程A,B它们的put的key的hash值相同,不论是从头插入还是从尾插入,假如A获取了插入位置为x,但是还未插入,此时B也计算出待插入位置为x,则不论AB插入的先后顺序肯定有一个会丢失
(2)在扩容的时候,jdk1.8之前是采用头插法,当两个线程同时检测到hashmap需要扩容,在进行同时扩容的时候有可能会造成链表的循环,主要原因就是,采用头插法,新链表与旧链表的顺序是反的,在JDK1.8后采用尾插法就不会出现这种问题,同时1.8的链表长度如果大于8就会转变成红黑树。
为什么ConcurrentHashMap是线程安全的?
ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。
ConcurrentHashMap让锁的粒度更精细一些,并发性能更好。
Hashtable
线程安全(所有操作在方法上面加上
synchronized
关键字),低效,不支持Null值和Null键
Hashtable的实现:
1 | public class Hashtable<K,V> |
LinkedHashMap
HashMap的子类,保存了记录的插入顺序。
Hashmap和Hashtable
HashMap不是线程安全的,Hashtable是线程安全的(方法被synchronized修饰)
HashMap允许nul的键值。
- HashMap继承自AbstractMap,Hashtable继承自Dictionary
反射
反射:程序执行期间才知道操作的类是什么,并能在运行期间获得类的完整构造(结构),并可调用方法。
小例子
Apple.java
1 | package Reflex; |
Demo.java
1 | package Reflex; |
常用方法
1 | //得到Class对象的3种方法 |
1 | //通过Class获得类实例对象的2种方式 |
1 | //通过Class获取类的属性 |
实战方法
在真正实战中如果面对一个完全未知的类Class,想要通过反射操作此类或对象,那么最常用的是以下方法
1 | //通常运用反射时,要向方法中传入xxx.class,所以我们已经拿到了Class |
我们通过拿到的所有方法,找到我们想要的方法,然后获取参数类型,就可执行对应方法。
反射结合POI实战,请移步个人原创:https://github.com/hanhanhanxu/HPoiUtil
多线程
请参见Thread.md
文件IO流
File
File既可以表示文件,也可以表示文件夹
windows的路径分隔符为 \ (因为一个 \ 代表转义字符,所以要使用两个 \ \,第一个 \ 是转义字符,将第二个 \ 转换为它本来的意思),Unix/Liunx下的路径分隔符为 / 。
1 | //利用File创建文件 |
更专业的方法是使用File.separatorChar
,它会根据当前操作系统自动匹配路径分隔符。
1 | System.out.println(File.separatorChar);// \ |
通过File遍历某盘/目录下的所有文件:
1 | package IO; |
File可以代表存在或不存在的文件/目录,存在的话可以操作此文件/目录,不存在的话可以创建此文件/目录。
这里的操作只是操作文件的基本信息。如果想要操作文件内部的内容,就需要用到IO流了。
码表
ASCII 美国标准信息交换码。用一个字节的7位表示字符,表示的都是英文字符和数字。
ISO8859-1 拉丁码表,又称Latin-1。欧洲码表,用一个字节的8位表示。ASCII码是包含的仅仅是英文字母,并且没有完全占满256个编码位置,所以它以ASCII为基础,在空置的0xA0-0xFF的范围内,加入192个字母及符号,藉以供使用变音符号的拉丁字母语言使用。从而支持德文,法文等。因而它依然是一个单字节编码,只是比ASCII更全面。
GB2312 英文占一个字节,中文占两个字节。中国的中文编码表。
GBK 中国的中文编码表升级,融合了更多的中文文字符号。
Unicode 国际标准码规范,融合了多种文字,所有文字都用两个字节表示。Java语言使用此码表。
UTF-8 万国码。1~3个字节不等长,英文1字节,中文3字节。最多用三个字节来表示一个字符。
编码:字符串—-》字节数组。String类的getBytes()方法进行编码,将字符串转换为对应的二进制,并且这个方法可以指定编码表,默认使用操作系统的码表。
中国大陆Windows系统默认码表一般为GBK,如果你设置了你的IDE的码表,那么就可能改变。Java程序中可以使用System.getProperty("file.encoding")
得到当前默认码表。
解码:字节数组—–》字符串。String类的构造函数完成。可以指定码表。我们使用什么码表编码,就应该使用什么码表解码,否则很有可能出现乱码。
IO
总述图
Closeable
1 | //Closeable 是可以关闭的数据源或目标。调用 close 方法可释放对象保存的资源(如打开文件)。 |
字节流:处理的单元是一个字节。
字符流:字节流+码表。
字节流
抽象父类InputStream
,OutputStream
子类FileInputStream
,FileOutputStream
,BufferedInputStream
,BufferedOutputStream
FileInputStream
1 | package IO; |
FileOutputStream
1 | package IO; |
构造器可以传入File,也可以传入String(代表文件路径) BufferedInputStream
1 | String filePath = "E:\\0面试\\a.txt"; |
字符流
抽象父类Reader
,Writer
子类FileReader
,FileWriter
,BufferedReader
,BufferedWriter
1 | package IO; |
转换流
需要额外传入一个码表
字节流通往字符流:InputStreamReader
字符流通往字节流:OutputStreamWriter
1 | package IO; |
拷贝文件
字节流拷贝
1 | package IO; |
字符流拷贝
1 | package IO; |
IO总结
何时使用字符流,何时使用字节流?依据是什么?
使用字符流的应用场景: 如果是读写字符数据的时候则使用字符流。
使用字节流的应用场景: 如果读写的数据都不需要转换成字符的时候,则使用字节流。
字节流
输入字节流: ———| InputStream 所有输入字节流的基类。
抽象类 ————| FileInputStream 读取文件的输入字节流
————| BufferedInputStream 缓冲输入字节流,其实该类内部只不过是维护了8kb的字节数组而已。 出现的目的主要是为了提高读取文件的效率。
输出字节流: ———| OutputStream 所有输出字节流的基类。
抽象类 ————–| FileOutputStream 向文件输出数据的输出字节流。
————–| BufferedOutputStream 向文件输出数据的输出字节流。
字符流
输入字符流: ———-| Reader 所有输入字符流的基类。
抽象类 ————–| FileReader 读取文件字符的输入字符流 。
————–| BufferedReader 缓冲输入字符流, 该类出现的目的主要是为了提高读取文件的效率与拓展功能(readLine)。
输出字符流 ———| Writer 所有输出字符流的基类。
抽象类。 ————-| FileWriter 向文件输出数据的输出字符流。
————-| BufferedWriter 缓冲输出字符流, 该类出现 的目的是为了提高写文件数据的效率与拓展功能。
转换流
输入字节流的转换流
InputStreamReader InputStream——————–> Reader
输出字节流的转换流
OutputStream OutputStream ——————–> Writer
转换流的作用:
- 可以把字节流转换成字符流使用。
- FileReader与FileWriter都是固定是gbk码表进行读写数据的,而转换流可以指定码表进行读写文件的数据。 Properties(配置文件类) 体系: ——-| Map ————| HashTable —————-| Properties 配置文件类、
store() 用于生成一个配置文件 load() 加载一个配置i文件 注意:
- 如果配置文件存在着中文,那么生成配置文件的时候要使用字符流,否则会出现乱码。
- 如果需要修改配置文件的内容,应该先加载原本配置文件,然后再生成一个配置文件。
class文件怎么读
除了javac还有哪些常用的工具