Java基础
一、jdk
1.java开发工具包
(包含 jre(java运行环境) 和 jvm(java虚拟机))
- JAVA_HOME: JDK的安装路径,例如:D:\Java\jdk-11.0.3 设置此变量的作用是:可以方便地修改JDK的安装路径,以免频繁修改系统path变量造成的风险。
- path: %JAVA_HOME%\bin;%path% 作用是:使得系统可以在任何路径下都可以识别由JDK提供的外部命令,如javac、java等(命令行程序)。
- classpath: 设定 经过javac编译后得到的.class文件所在的路径,例如:.;c:\myclass 作用是:在任何路径下运行java classfile命令时,Java虚拟机都可以找到该class文件并运行。
2.java程序运行
java两种核心机制:java虚拟机跟垃圾回收机制。 java程序运行需要依靠java虚拟机。
二、变量
1.变量的存储
int a = 1; 在计算机内存中划分一个大小为32的内存空间,并命名为a,将1放入这块内存空间中
基本数据类型的变量直接存储值,但是引用数据类型(String和其它自定义数据类型的对象)存储的是实际变量在堆中的地址。一个原则:方法体中的引用变量和基本类型的变量都在栈上,其他都在堆上。 成员变量作为对象的属性,是放在堆里了,即使它是属于基本数据类型的变量。对象在堆里,对象中的内容就是各种字段。
java类中所有public和protected的实例方法都采用动态绑定机制,所有私有方法、静态方法、构造器及初始化方法都是采用静态绑定机制。而使用动态绑定机制的时候会用到方法表,静态绑定时并不会用到。
2.java内存模型
什么是java的内存模型?
2.1 成员变量的操作
JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
线程1和线程2要想进行数据的交换一般要经历下面的步骤: 1.线程1把工作内存1中的更新过的共享变量刷新到主内存中去。2.线程2到主内存中去读取线程1刷新过的共享变量,然后copy一份到工作内存2中去。
2.2 操作的原则
可能存在的问题:两个线程对变量的操作和刷新到主内存中是相互独立的,可能会存在类似不可重复读的情况。
Java内存模型是围绕着并发编程中原子性、可见性、有序性这三个特征来建立的 :
- 原子性:数据不可分割,例如对于32位的jvm而言,当读取64位的数据类型的变量时,会将数据分为高32位和低32位读取,如果是多线程并发执行时,可能会造成安全问题。
- 可见性:一个线程修改了共享变量,其它线程能立马感知到。被volatile修饰的变量可以保证可见性,因为volitile变量每次被使用时都会直接从主内存中读取数据,这样就保证了每次使用的数据都是最新的,同时具有此特性的变量还有synchronized ,lock,final修饰的变量。
- 有序性:对于单个线程执行代码而言,是从前往后依次按顺序执行;但是在多线程并发时,程序的执行就有可能出现乱序。 用一句话可以总结为:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。
java中提供了volatile关键字和synchronized关键字实现有序性,其中volatile通过内存屏障实现,synchonized通过加锁实现同步保证有序性。
2.3 happens-before
衡量线程并发安全问题:happens-before原则
Java内存模型中定义的两项操作之间的次序关系,如果说操作A先行发生于操作B,操作A产生的影响能被操作B观察到,“影响”包含了修改了内存中共享变量的值、发送了消息、调用了方法等。
下面是Java内存模型下一些”天然的“happens-before关系,这些happens-before关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随意地重排序。
a.程序次序规则(Pragram Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。
b.管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而”后面“是指时间上的先后顺序。
c.volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读取操作,这里的”后面“同样指时间上的先后顺序。
d.线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
e.线程终于规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等作段检测到线程已经终止执行。
f.线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。
g.对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始。
g.传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
一个操作”时间上的先发生“不代表这个操作会是”先行发生“,那如果一个操作”先行发生“是否就能推导出这个操作必定是”时间上的先发生 “呢?也是不成立的,一个典型的例子就是指令重排序。所以时间上的先后顺序与happens-before原则之间基本没有什么关系,所以衡量并发安全问题一切必须以happens-before 原则为准。
2.4 内存屏障
内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。
java 的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。
(Load 指令(也就是从内存读取),Store指令 (也就是写入内存)。)
- LoadLoad 屏障:对于这样的语句 Load1; LoadLoad; Load2 ,在 Load2 及后续读取操作要读取的数据被访问前,保证Load1 要读取的数据被读取完毕。
- StoreStore 屏障:对于这样的语句 Store1; StoreStore; Store2 ,在 Store2 及后续写入操作执行前,保证 Store1 的写入操作对其它处理器可见。
- LoadStore 屏障:对于这样的语句 Load1; LoadStore; Store2 ,在 Store2 及后续写入操作被刷出前,保证 Load1 要读取的数据被读取完毕。
- StoreLoad 屏障:对于这样的语句 Store1; StoreLoad; Load2 ,在 Load2 及后续所有读取操作执行前,保证 Store1 的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
volatile通过内存屏障实现了有序性(禁止指令重排序),JVM做了什么?- JVM在volatile修饰的变量写操作之前添加了StoreStore屏障,在之后添加了LoadStore
- JVM在volatile修饰的变量读操作之前添加了LoadLoad屏障,在之后添加了StoreLoad
char类型能否存储汉字?
可以;java采用的Unicode编码,而汉字也是采用的Unicode收录的,如果存储的汉字是被收录的就可以存储,否则不能存储。
三、浅拷贝与深拷贝
Java 中的数据类型分为基本数据类型和引用数据类型。 在参数传递时会有值传递和“引用传递”两种情况。
java中只有值传递!
1. 直接赋值
如果是基本数据类型直接赋值不会有影响,但是直接赋值的是引用数据类型就会产生问题,这时赋值的是引用数据的地址,两个变量指向同一块内存地址,当一个变量修改时也会影响到另一个变量。
2. 浅拷贝
浅拷贝是按位拷贝对象,它会创建一个新的对象,但是这个对象有着原始对象属性值的一份精确拷贝。 如果属性是基本数据类型,就拷贝属性值;如果是内存地址(引用数据类型),拷贝的就是内存地址,这和直接赋值中产生的问题一致,当一个对象去修改这个内存地址就会影响到另外一个对象,默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
浅拷贝的实现
实现浅拷贝功能对象的类,需要实现 Cloneable 接口,并覆写 clone() 方法。
@Override
protected Object clone() throws CloneNotSupportedException {
try {
return super.clone();
} catch (CloneNotSupportedException e){
e.printStackTrace();
return null;
}
}
浅拷贝依旧调用的Object提供的默认clone方法。
浅拷贝会带来数据安全方面的隐患 ,所以我们需要深拷贝的存在。
3. 深拷贝
-
深拷贝也会直接创建一个对象,与之不同的是对引用数据类型的拷贝:
-
深拷贝对于基本数据类型:值传递,直接将属性值赋给拷贝对象。彼此之间不会相互影响。
-
深拷贝对于引用数据类型:比如数组或者类对象 ,与浅拷贝不同,深拷贝会直接创建一个对象或数组的内存空间,将里面的内容进行拷贝,这样修改时彼此之间就不会相互影响了。
@Override
public Object clone() {
//如果成员有引用数据类型的需要多层拷贝
try {
//直接调用父类的clone()方法
DeepCloneTest deepCloneTest = null;
try {
deepCloneTest = (DeepCloneTest) super.clone();
deepCloneTest.user = (User) user.clone(); //User类也需要实现clone方法
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}} }
值得注意的是:对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法,进而实现了对象的串行层层拷贝。 深拷贝相比于浅拷贝速度较慢并且花销较大。
四、自动装箱和拆箱
Integer----int (4字节)
Short—short (2字节)
Byte—byte (1字节)
Boolean—boolean (1字节)
Character—char (2字节)
Double—double (8字节)
Float—float (4字节)
Long—long (8字节)
装箱和拆箱过程都是自动执行的。
1. 装箱
装箱就是基本类型转为包装器类型。
自动装箱调用的是valueOf()方法,以Integer为例:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Integer 中有一个静态内部类 IntegerCache,在静态代码块中维护了一个缓存数组,存放了-128到127的Integer对象(Boolean类也提前创建了两个对象对应true和false),调用该方法时首先会判断该值是否在缓存的范围内,如果在则直接将缓存中的数值返回,否则返回一个新对象。 装箱的过程会创建对应的对象,这个会消耗内存,所以装箱的过程会增加内存的消耗,影响性能。
创建Integer对象
private final int value;
public Integer(int value) {
this.value = value;
}
public Integer(String string) throws NumberFormatException {
this(parseInt(string));
}
Integer i1 = 127;
Integer i2 = new Integer(128);
Integer i3 = 127;
System.out.println(i1==i3); //true
System.out.println(i1==i2); //false
2. 拆箱
拆箱就是包装器类型转为基本类型。
最常见的就是数值的基本运算
重写equals()方法一定要重写hashcode()方法:
包装类在用equals比较时同时比较类型和值。Integer类的hashcode方法被重写,返回的是对应基本类型的值。
五、异常
Error一般是非代码性错误,是程序中无法处理的错误,表示运行应用程序中出现了严重的错误。 比如OOM。
Exception:程序本身可以捕获并且可以处理的异常。 是需要我们在开发中需要注意的。
1. 运行时异常 (JVM负责)
运行时异常(不受检异常):RuntimeException类极其子类表示JVM在运行期间可能出现的错误。编译器不会检查此类异常,并且不要求处理异常,比如用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
2. 非运行时异常(编译器检查)
非运行时异常(受检异常):Exception中除RuntimeException极其子类之外的异常。编译器会检查此类异常,如果程序中出现此类异常,比如说IOException,必须对该异常进行处理,要么使用try-catch捕获,要么使用throws语句抛出,否则编译不通过。
3. 自定义异常
除了JDK定义好的异常类外,在开发过程中根据业务的异常情况自定义异常类。
* @author:
* @date: 2020/9/17 21:15
* @description: 自定义异常
*/
public class UserNotExistsException extends RuntimeException {
public UserNotExistsException() {
}
public UserNotExistsException(String message) {
super(message);
}
}
六、集合框架(Iterator/Collection/Map)
数组与集合的区别如下:
1)数组长度不可变化而且无法保存具有映射关系的数据;集合类用于保存数量不确定的数据,以及保存具有映射关系的数据。
2)数组元素既可以是基本类型的值,也可以是对象;集合只能保存对象。
1. 泛型
泛型的本质就是参数化类型;泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法
ArrayList list = new ArrayList();
list.add(100);
list.add("aaaa");
for(int i=0;i<list.size();i++){
System.out.println((String) list.get(i));
}
上边例子中没有使用泛型时list中既能存储String,也能存储int,但是读取时都以String类型读取就会报错。
2. Collection
Collection派生出了三个子接口:List、Set、Queue(Java5新增的队列)
2.1 List
List接口一共有三个实现类,分别是ArrayList、Vector和LinkedList ,List用于存放多个元素,能够维护元素的次序,并且允许元素的重复。
ArrayList:内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是扩容需要重新创建一个新数组,且插入删除的代价相对较高,适合查找(可以直接通过下标查询)和遍历。
/**
* Default initial capacity. 默认初始化容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances. 用于空实例的共享空数组实例
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 我们将其与EMPTY_ELEMENTDATA区分开来,以便知道何时膨胀多少
* 添加第一个元素。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
//transient:不被序列化
思考:
1、为什么在ArrayList中定义了两个空数组?****
区别在于new ArrayList(0) 和 new ArrayList()两种不同的情况
- 当创建ArrayList时不传入参数(即不给容量)new ArrayList(),ArrayList默认容量大小为10,在之后会自动分配给创建的list,这是实际是将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋给list。
- 当创建ArrayList传入参数为0时,这个时候要与DEFAULTCAPACITY_EMPTY_ELEMENTDATA分开区别,所以定义了EMPTY_ELEMENTDATA 来区分,在之后也不会自动分配默认容量。
2、ArrayList扩容原理
ArrayList通过调用grow()方法实现扩容;
扩容时机: 当数组的大小大于初始容量的时候(比如初始为10,当添加第11个元素的时候),就会进行扩容,新的容量为旧的容量的1.5倍。
扩容方式 : 扩容的时候,会以新的容量建一个原数组的拷贝,修改原数组,指向这个新数组,原数组被抛弃,会被GC回收。
LinkedList:List接口的另一个实现,除了可以根据索引访问集合元素外,LinkedList还实现了Deque接口,可以当作双端队列来使用,也就是说,既可以当作“栈”使用,又可以当作队列使用。LinkedList的实现机制与ArrayList的实现机制完全不同,ArrayLiat内部以数组的形式保存集合的元素,所以随机访问集合元素有较好的性能;LinkedList内部以链表的形式保存集合中的元素,所以随机访问集合中的元素性能较差,但在插入删除元素时有较好的性能。
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
值得注意的是LinkedList也可以通过下标查询,但是查询方式是依次遍历(为了效率,会先判断下标选择从头还是尾开始遍历)。
LinkedList linkedList = new LinkedList();
linkedList.add("a");
linkedList.add("b");
linkedList.add("c");
System.out.println(linkedList.get(2));
Vector和ArrayList操作方法类似,底层都是数组实现的;差别在于Vector是线程安全的,ArrayList是线程不安全的,当不需要考虑多线程并发的问题时,使用ArrayList效率更高。
Stack中的peek()方法和pop()方法都能弹出数据,差别在于pop会在取出数据后在栈中删除数据,peek只是获取到数据,但是不会删除。
2.2 Set
Set接口的主要实现类有HashSet和TreeSet
-
HashSet的实现原理是通过内部维护了一个HashMap
-
为什么HashSet内部的HashMap的Value值不能为null而是一个对象?
因为HashSet在加入相同的值时会返回false,它的值是通过将对象存入内部维护的HashMap中得到的返回值进行比较;在HashMap中,是允许Value值为null的。如果前边add时,添加的value为null,那返回的话,就不能区分时第一次添加返回的空值null,还是重复添加时,返回的已有值,只是这个值为null,但是,添加的如果是一个Object,就可以避免这个问题了。
public boolean add(E e) {
return map.put(e, PRESENT)==null; //PRESENT是一个被static和final修饰Object对象
}
TreeSet时SortedSet接口的实现类,TreeSet可以保证元素处于排序状态,它采用红黑树的数据结构来存储集合元素。TreeSet支持两种排序方法:自然排序和定制排序,默认采用自然排序。 如果试图将一个对象添加到TreeSet集合中,则该对象必须实现Comparable接口,否则会抛出异常。
想要实现定制排序,需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由Comparator对象负责集合元素的排序逻辑。
综上:自然排序实现的是Comparable接口,定制排序实现的是Comparator接口。
2.3 Map
最常用的是HashMap,HashMap提供高效的查询、插入和删除。HashMap是线程不安全的,但是集合框架提供了方法可以实现线程安全:
Map m = Collections.synchronizeMap(hashMap);
存储原理:
-
HashMap底层以数组的方式存储,按键值对(key-value)的形式作为一个数组元素存储到Entry数组中。
-
存储时,通过计算hash值,然后与Entry数组长度-1进行“与”运算得到最终的存储下标。
-
虽然通过“与”运算已经减少了hash冲突,但不能完全避免,所以HashMap采用了数组+链表(jdk1.8之前),在jdk1.8中采用的数组+链表+红黑树。
-
HashMap的hash值与hashcode()方法有关,但又不是全由hashcode方法决定:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hashcode方法计算后得到的值h与h逻辑右移16位进行异或运算才是最终的hash值
当hash冲突较大时,可以重新实现hash算法。/**
- 默认初始化容量 - 必须是2的幂.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
- 最大容量2的30次幂
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
如果未在构造函数中指定负载因子,就使用默认的0.75.
/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/
*当链表长度达到8时链表转为红黑树
*/
static final int TREEIFY_THRESHOLD = 8;/**
- 当红黑树元素个数为6时转为链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
- 默认初始化容量 - 必须是2的幂.
思考:
-
HashMap 的 hash 算法的实现原理(为什么右移 16 位,为什么要使用 ^ 位异或)
从程序员的角度,这是为了更好的均匀散列表的下标,减少hash冲突 -
HashMap 为什么使用 & 与运算代替模运算?
当数组长度为2的幂的时候,与运算的效率比取模高(未深入研究) -
HashMap 的容量为什么建议是 2的幂次方?
在计算数组下标时进行了与运算(取模),当容量是2的幂次方时效率更高。在HashMap的自动扩容中,扩容为原来的2倍(容量左移1位,相当于乘2)一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模预算(据说提升了5~8倍)。
-
我们自定义 HashMap 容量最好是多少?
-
为什么使用Integer、String这样的包装类作为KEY是非常好的?
被final修饰的不可变的对象,保证存放到HashMap后不会再改变,并且重写了equals和hashcode方法。总结HashMap中存储下标的计算(非hash值):
方法一:
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //得到的是hash值如果key为null直接返回0
方法一得到hash值分为两步:1、调用hashcode()方法取hashcode值;2、hashcode值的高16位参与运算。
方法二:取模运算
static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
return h & (length-1); //第三步 取模运算
}
这里要注意的是与运算和取模的关系:与n取模其实就是和n-1相与 。
只要hashcode()方法得到的值相等,方法一得到的hash值总是相等的;为了使元素分布相对均匀,我们将得到的hash值进行取模从而得到下标,但是取模的开销较大,与此同时,当数组长度为2的幂时,hash值与数组长度进行取模的计算可以用效率更高的与运算来代替。
在jdk1.8优化了高位运算:高16位异或低16位,这样可以保证在table较小的时候高低位也都能参与计算,同时没有太大的开销。
当发生hash冲突,且该链表长度为8时,会先”尝试“转为红黑树(先去判断当前容量是否达到阈值64,如果没有会放弃本次转换,优先对数组进行扩容。
多线程下重新调整HashMap大小,存在竞争的问题,如果发生了竞争,会造成循环链表。
疑惑:在HashMap扩容(resize)之后,把元素添加到新的数组中,重新计算了下标,那链表或者红黑树怎么实现复制过去的?
2.4 比较器
Comparable和Comparator接口都是为了对类进行比较,众所周知,诸如Integer,double等基本数据类型(实现了内部比较器comparable接口),java可以对他们进行比较,而对于类的比较,需要人工定义比较用到的字段比较逻辑。可以把Comparable理解为内部比较器,而Comparator是外部比较器 (compare方法在外部被调用)。
Collections类是一个包装类,它包含有各种有关集合操作的静态方法。就像一个工具类。
Collections.sort()
sort()排序方法,根据元素的自然排序对指定列表按升序进行排序
public static void sort(List list,Comparator<>),根据指定比较器产生的顺序对指定列表进行排序,此列表内的所有元素都必须可使用指定的比较器相互比较
参数:list——要排序的列表
C——确定列表顺序的比较器
public class AnimalComparator implements Comparator<Animal> {
@Override
public int compare(Animal o1, Animal o2) {
return o1.getId()-o2.getId();
}
/*
* 这是说如果o1的id - o2的id是正数就升序,如果负数降序。如果0就剔除
>=1 升序
<=-1 降序
=0 重复,不记录
*/
}
基于hash存储的对象需要实现内部比较器
七、IO流
java的流按流向分可以分为输入流和输出流,是以我们写的程序或者内存为参照,将数据读取到内存的流即输入流,将数据从内存中写出到文件或硬盘的流叫输出流。也可以按数据传输单位分为字节流和字符流。IO流中的设计模式即装饰者模式和适配器模式。
字符流和字节流的使用范围 :字符流一般用来处理文本类文件,但是字节流既可以处理文本类文件也可以处理非文本类文件(音频类、图片类等)
字符流与字节流转换
转换流的作用,文本文件在硬盘中以字节流的形式存储时,通过InputStreamReader读取后转化为字符流给程序处理,程序处理的字符流通过OutputStreamWriter转换为字节流保存。
1. 输入流
所有的输入流都继承自InputStream或Reader
2. 输出流
所有的输出流都继承自OutputStream或Writer
输入输出流使用demo:
File file = new File("src/01.png");
if(!file.exists()){
file.createNewFile();
}
FileInputStream in = new FileInputStream(new File("E:/01.png"));
FileOutputStream out = new FileOutputStream(file);
byte[] b = new byte[1024];
int n = 0;
while ((n = in.read(b))!=-1){
out.write(b,0,n);
}
in.close();
out.close();
包装流的使用:
File file = new File("src\\用户信息.txt");
if(!file.exists()){
throw new FileNotFoundException("文件不存在");
}
FileInputStream fin = new FileInputStream(file);
//使用包装流
BufferedReader reader = new BufferedReader(new InputStreamReader(fin));
String str = "";
while ((str = reader.readLine())!=null){
System.out.println(str);
}
3. IO流中用到的设计模式
3.1 IO中的适配器模式
适配器模式作为两个不兼容的接口之间的桥梁,比如InputStream不能像Reader字符流那样方便的读取文本文件,因此借助InputStreamReader类将字节流转为字符流,拥有便捷的操作文本文件的方法,OutputStream同理。
3.2 IO中的包装者模式
包装者模式在于向现有的对象添加新的功能,但不改变其内部结构。InputStream在经过包装成Reader拥有了新的方法,Reader再经过包装为BufferReader后又拥有了新的方法。
总结:
适配器模式主要是为了接口的转换,而装饰者模式关注的是通过组合来动态的为被装饰者注入新的功能或行为。
适配器将一个对象包装起来以改变其接口;装饰者将一个对象包装起来以增强新的行为和责任。
4. 序列化和反序列化
序列化时,只对对象的状态进行保存,而不管对象的方法和类的状态(static修饰的)
4.1 序列化和反序列化的定义
(1) Java序列化就是指把Java对象转换为字节序列的过程。
Java反序列化就是指把字节序列恢复为Java对象的过程。
(2) 序列化最重要的作用:在传递和保存对象时.保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。
反序列化的最重要的作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。
总结:核心作用就是对象状态的保存和重建。(整个过程核心点就是字节流中所保存的对象状态及描述信息)。
4.2 Java实现序列化和反序列化的过程
1、实现序列化的必备要求:
只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)
2、JDK中序列化和反序列化的API:
①java.io.ObjectInputStream:对象输入流。
该类的readObject()方法从输入流中读取字节序列,然后将字节序列反序列化为一个对象并返回。
②java.io.ObjectOutputStream:对象输出流。
该类的writeObject(Object obj)方法将将传入的obj对象进行序列化,把得到的字节序列写入到目标输出流中进行输出。
3、 实现序列化和反序列化的三种实现:
①若Student类仅仅实现了Serializable接口,则可以按照以下方式进行序列化和反序列化。
ObjectOutputStream采用默认的序列化方式,对Student对象的非transient的实例变量进行序列化。
ObjcetInputStream采用默认的反序列化方式,对Student对象的非transient的实例变量进行反序列化。
②若Student类仅仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),则采用以下方式进行序列化与反序列化。
ObjectOutputStream调用Student对象的writeObject(ObjectOutputStream out)的方法进行序列化。
ObjectInputStream会调用Student对象的readObject(ObjectInputStream in)的方法进行反序列化。
③若Student类实现了Externalnalizable接口,且Student类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。
ObjectOutputStream调用Student对象的writeExternal(ObjectOutput out))的方法进行序列化。
ObjectInputStream会调用Student对象的readExternal(ObjectInput in)的方法进行反序列化。
八、String类
-
String类的equals方法
public boolean equals(Object anObject) { //形参是Object
if (this == anObject) { //先比较的是传入对象的地址,如果两个对象引用指向的地址一样,那就是同一个对象
return true;
}
if (anObject instanceof String) { //判断传入对象是否是String类型
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) { //比较两个字符串长度,长度一样再仔细比较
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i]) //逐个比较字符串的字符
return false;
i++;
}
return true;
}
}
return false;
}
String类的equals方法已经重写了,并不是根据hashcode方法的返回值进行判断,而是比较的字符串的值。
- 重写后的hashcode方法
重写equals方法一定要重写hashcode()方法,String的hashcode方法还是比较简单的,就是以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模。
哈希计算公式可以计为s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1]
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
31是一个奇质数,所以31 x i=32 x i - i=(i<<5) - i,这种移位与减法结合的计算相比一般的运算快很多。 - 字符串常量池
采用字面量赋值的方式创建一个字符串时,JVM会先在字符串常量池中查找是否存在这个字符串,如果不存在,则在字符串常量池中创建后返回其地址,否则直接返回该字面量在常量池中的地址。
(jdk1.7及以后)当调用 intern方法时,如果字符串常量池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用。
九、多线程
1.线程的状态
线程分为6种状态(参照的jdk中源码)
1.初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2.运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3.阻塞(BLOCKED):表示线程阻塞于锁。
4.等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5.超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6.终止(TERMINATED):表示该线程已经执行完毕。
在 Java 中常用实现多线程有两种手段,一种是继承 Thread 类,另一种就是实现 Runnable 接口。 两种实现方式都需要重写run()方法。当然如果是面试中问到那还要说出实现Callable接口和线程池的方式创建线程。
使用多线程想要实现的目的是程序更快的完成任务,多线程并发时,必须要实现的是线程通信和线程同步;线程通信指的是不同线程之间共享数据,线程同步指的是不同线程执行的先后顺序。
实现Java代码锁比较简单,一般使用两个关键字对代码进行线程锁定。最常用的就是volatile和synchronized两个。 synchronized关键字修饰的相当于数据库上的互斥锁,确保多个线程在同一时间只有一个能访问代码块或方法,获得锁的对象在执行完操作后会释放锁。
synchronized关键字一般加在方法上对方法进行上锁,或者定义一个synchronized代码块,值得一提的是可以用javap命令去看字节码文件,同步方法是在方法头部加上了一个ACC_SYNCHRONIZED标识符,同步代码块则是通过monitorenter和monitorexit来使jvm知道这段代码块是需要同步的。
十、常用类
- Data(日期类)
//
- Calendar(日历类)
//
- Math
//
写一个日历的demo:
1.用数组来存储每一天
2.将日期设置为该月的第一天
3.求第一天是星期几,方便确认显示上一个月的几天
4.求上个月最大天数
5.计算上一月显示日期,存入数组
6.计算本月天数,存入数组
7.循环打印
十一、设计模式
-
单例模式
package cn.qmlin.mode;
/**
-
@author:
-
@date: 2020/9/16 9:03
-
@description: 单例设计模式
/
public class Singleton {
/
Singleton singleton = new Singleton()
对象初始化分三步
1.给对象在堆中分配内存空间
2.初始化对象
3.将对象的引用给singleton
指令重排序可能造成1->3->2,这样拿到的就是为初始化完成的对象,但是不为null,程序判断通过,之后不会再创建对象。并发原因是因为两次检查是可能并发的
*/
//使用volatile关键字的作用是因为可以保证可见性和有序性(禁止指令重排)
private static volatile Singleton instance;//构造函数私有化保证单例
private Singleton() {
}/**
- @date: 2020/9/16 9:06
- @description: 在单线程中可以保证“单例”,但是多线程并发访问判断时,可能会破坏单例模式;
- 我们可以通过加锁来保证对象唯一性
*/
public static Singleton getInstance() { //不在方法头上加锁的原因是为了减少同步开销,也就引出了double-check
if (instance == null) { //直接在该方法体上加锁会降低并发访问效率,所以可以在外层多加一层判断
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
-
-
工厂模式
//工厂模式、代理模式、观察者模式等在学习Spring的过程中可以看下框架的实现
3.代理模式
//
- 包装模式
// 包装者模式和适配器模式都是在IO流中有应用的
- 观察者模式
//
十二、反射
反射的内容并不多,需要注意的是面试中会问到的可以通过反射创建对象,还有反射的优点是我们的编程更灵活,可以在程序运行过程中去修改对象,但是这也会导致不安全性。使用反射的效率是比直接运行的效率略低的,但是框架中更需要的是编程的扩展性。
十三、Java内存结构和GC
堆、本地方法栈、虚拟机栈、元空间、程序计数器PC
在 JAVA 语言中有8中基本类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个JAVA系统级别提供的缓存。
因为时间和篇幅问题,简单写一下自己面试中经常问到的:
首先是堆,new的对象都是存放在堆中的,这个部分也是GC的主要区域,讲到GC的时候,肯定会想到的是几种内存收集器和GC涉及到的几种算法(标记清除和标记整理算法),在jdk1.8之后移除了方法区,使用的是元空间的概念,它使用的是机器的直接内存,还有new对象分配内存时有指针碰撞和空闲列表两种方式(具体可以单独去看)。
然后是栈,栈是私有的,这个特性也可以和volatile解决的可见性的问题等联系起来,建议单独去看。