补充自己的弱项,尤其是一些概念很模糊。
我接触过的技术:
spring
springmvc
mybatis
mybatisplus
lomback:在项目中使用Lombok可以减少很多重复代码的书写。比如说getter/setter/toString等方法的编写。
springboot
shiro
netty
nio
rocketmq
vue
thyemleaf
感慨:
学了那么多的技术,到头来脑子里面回想起来发现记得很少,很多细节什么的具体的语法都忘了,虽然当时记得很清楚。
不过也留下了一点东西:经验。
项目报错,可能的原因在哪。对项目的认知程度有所提升。
JavaSE基础
==和equals
hashcode与equals
标识符
Java 中标识符是为方法、变量或其他用户定义项所定义的名称。标识符可以有一个或多个字符。在 Java 语言中,标识符的构成规则如下。
- 标识符由数字(09)和字母(AZ 和 a~z)、美元符号($)、下划线(_)以及 Unicode 字符集中符号大于 0xC0 的所有符号组合构成(各符号之间没有空格)。
- 标识符的第一个符号为字母、下划线和美元符号,后面可以是任何字母、数字、美元符号或下划线。
另外,
Java 区分大小写
,因此 myvar 和 MyVar 是两个不同的标识符。
提示:标识符命名时,切记不能以数字开头,也不能使用任何 Java 关键字作为标识符,而且不能赋予标识符任何标准的方法名。
标识符分为两类,分别为关键字和用户自定义标识符。
-
关键字是有特殊含义的标识符,如 true、false 表示逻辑的真假。
关键字也是一种标识符
-
用户自定义标识符是由用户按标识符构成规则生成的非保留字的标识符,如 abc 就是一个标识符。
提示:使用标识符时一定要注意,或者使用关键字,或者使用自定义的非关键字标识符。此外,标识符可以包含关键字,但不能与关键字重名。
例如以下合法与不合法标识符。
合法标识符:date、$2011、_date、D_$date 等。
不合法的标识符:123.com、2com、for、if 等。
标识符用来命名常量、变量、类和类的对象等。
因此,一个良好的编程习惯要求命名标识符时,应赋予它一个有意义或有用途的名字。
数组
java的api中,并没有提供删除数组中元素的方法。虽然数组是一个对象,不过并没有提供add()、remove()或查找元素的方法。这就是为什么类似ArrayList和HashSet受欢迎的原因。
1.数组的创建需要指定元素个数;int[] arr = new int[3];
2.对象.length获取数组 长度; length()方法是字符串获取长度的方式。
3.数组里面可以存储基本数据类型、引用数据类型;而集合里面只能存储引用类型。
4.数组遍历
两种遍历方式:for和增强for循环。
集合外加迭代器遍历(仅限conllection接口下的集合)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EwesAqpK-1588857152670)(img/TIM截图20200503222617.png)]
5.如果你想对数组里面的元素做更多的操作,一种方式就是可以把数组转为集合List list = Arrays.asList(arrs)
集合的功能很多,增删改查均可使用。
集合转数组list.toArray(arrs):
谈谈对OOP的理解
1首先面向对象是一种编程思想
2万物皆对象。我的电脑是一个对象,我的手机是一个对象等等,OOP可以理解为使用代码来模拟现实生活
3三大特性:封装、继承和多态
1封装就是隐藏类的内部信息,不允许外部程序直接访问,而是通过getter(获取)和setter(设置)方法完成,提高数据的安全性
2继承是指:父类的基本特征和行为,子类也会有,子类也可以改变这些特征和行为。
3多态就是多个对象调用同一个方法,可能会得到的是不同的结果。
String
String底层使用什么实现的?为什么不可变?
String的底层使用的是char数组。这个char数组和String这个类都是final修饰的,所以不可变。
String,StringBuilder,StringBuffer的区别?
String是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个final类型的字符数组,所引用的字符串不能被改变,一经定义,无法再增删改。每次对String的操作都会生成新的String对象。
String的每次+操作 : 隐式在堆上new了一个跟原字符串相同的StringBuilder对象,再调用append方法 拼接
+后面的字符,最后再调用toString方法,返回String对象
StringBuffer和StringBuilder他们两都继承了AbstractStringBuilder抽象类,从AbstractStringBuilder抽象类中我们可以看到他们的底层都是可变的字符数组,所以在进行频繁的字符串操作时,建议使用StringBuffer和StringBuilder来进行操作。
另外StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的
日期
格式化日期用的什么?
使用的是 SimpleDateFormat 。
把日期转换为指定格式的字符串
//使用SimpleDateFormat类,在构造方法中指定日期的格式:y年,M月,d日,H小时,m分钟,s秒
SimpleDateFormat sdf = new SimpleDateFormat("yyy y-MM-dd HH:mm:ss");
//调用SimpleDateFormat实例方法format(Date)可以把日期转换为字符串
String text = sdf.format(date);
把字符串转换为日期
text = "2068年5月12日 8:28:58";
//先创建SimpleDateFormat对象, 指定的格式串要与字符串匹配
SimpleDateFormat anotherSdf = new SimpleDateFormat("yyyy年M月dd日 H:mm:ss");
//调用SimpleDateFormat的实例方法parse(String)可以把字符串转换为日期对象, 如果格式串与字符串不匹配就会产生异常
date2 = anotherSdf.parse(text);
IO流
Java缓冲流
一开始,并不明白java中的缓冲流到底有什么作用。OutputStream和BufferedOutputStream有什么区别,两者的write()方法都是一个字节一个字节的写,不同之处就是BufferedOutputStream会将字节先写到一个缓冲区中,然后通过==flush()==或者关闭这个缓冲流来实现将缓冲流中的数据回显。
之后,通过查阅关于缓冲流的相关知识,了解了一点点,一个字节一个字节的写入到硬盘会耗费时间,而先把数据写入缓冲区的话,再一次性的写入到硬盘或者其他地方,会节省很多时间。从而提高效率。
IO流中都有哪些类?
节点流(介质流):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yNicFnop-1588857152674)(img/TIM截图20200501064714.png)]
处理流(装饰流):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q5kz3B8l-1588857152676)(img/TIM截图20200501064803.png)]
对象流也可以叫做序列化流。
面向对象编程的六大原则
开闭原则:
开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。
里氏替换原则:
所有引用基类的地方必须能透明地使用其子类的对象,也就是说子类可以扩展父类的功能,但不能改变父类原有的功能
里氏替换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏替换原则是对实现抽象化的具体步骤的规范。
依赖倒转原则:
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。简单的说就是尽量面向接口编程.
接口隔离原则:
客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。接口最小化,过于臃肿的接口依据功能,可以将其拆分为多个接口。
迪米特法则,又称最少知道原则:
一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。简单的理解就是高内聚,低耦合,一个类尽量减少对其他对象的依赖。
单一职责原则:
单一职责原则通俗来说就是一个类只负责一项职责。如果一个类有多个职责,这些职责就耦合在了一起。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起会影响复用性。
设计模式
了解哪些设计模式?具体说说其中的某个?
单例模式:懒汉式、饿汉式、双重校验锁、静态加载,内部类加载、枚举类加载。保证一个类仅有一个实例,并提供一个访问它的全局访问点。
代理模式:动态代理和静态代理,动态代理有jdk动态代理和cglib动态代理。
适配器模式:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
装饰者模式:动态给类加功能。
观察者模式:有时被称作发布/订阅模式,观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
策略模式:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
外观模式:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
命令模式:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。
创建者模式:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
集合
List,Set,Map区别(扩容过程)
(1)List集合
存储特点:有序,可重复。存储顺序与添加顺序一致,可以存储重复的数据
ArrayList
底层数据结构是数组,访问快,添加,删除慢
初始化容量:10
扩容:1.5倍
Vector
底层数据结构是数组,它是线程安全的,ArrayList不是线程安全的
初始化容量:10
扩容:2倍
LinkedList
底层是双向链表,访问慢,添加和删除效率高
(2)Set集合
存储特点:无序,不可重复。存储顺序与添加顺序可能不一样;不能存储重复(通过equals()方法来判断)的数据
HashSet
底层是HashMap
向HashSet添加元素就是把元素作为键添加到底层的HashMap中
TreeSet
TreeSet实现了SortedSet接口,可以根据元素自然排序,要求集合中的元素必须是可比较的(Comparator与Comparable)
TreeSet底层是TreeMap
向TreeSet添加元素就是把元素作为键添加到底层的TreeMap中
(3)Map
Map是按<键,值>对的形式存储数据,Map的key不允许重复
HashMap
底层是数组+链表,键与值都可以为null,线程不安全
JDK8中,当链表长度超过 8 时,链表转换为红黑树;插入时,新增的结点插入到链表的尾部
初始化容量为16
加载因子:0.75,
当键值对的数量 > 容量*加载因子时,按2倍大小扩容
在创建时可以指定初始化容量,HashMap会把初始化容量自动调整为2的幂次方
这边也可以引申到一个问题就是HashMap是先插入数据再进行扩容的,但是如果是刚刚初始化容器的时候是先扩容再插入数据。
ConcurrentHashMap
jdk1.7使用的是分段锁:segment+hashentry
jdk1.8底层采用数组+链表+红黑树,利用CAS+Synchronized来保证线程安全
详见:
HashTable
底层数组+链表,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
初始容量为11,扩容:2倍+1
HashMap遍历怎么遍历?
两种方式遍历:使用EntrySet遍历;使用KeySet 遍历
平时什么情况下使用HashMap?
比如说读取系统参数,在服务之间传递一些键值对时使用。
简单说一下什么是hash碰撞?怎么解决的?
首先根据键的哈希码,经过hash函数计算hash值,然后根据hash值计算数组下标,当下标相同时,就会出现hash碰撞。
HashMap采用链表法,将数组下标相同的键值对用链表存起来,解决了hash碰撞。
TreeMap和LinkedHashMap有什么区别?
HashMap 相较来说写入慢,读取快,上面介绍过了,就不赘述了;
LinkedHashMap它内部有一个链表,保持Key插入的顺序,写入快,读取慢。迭代的时候,也是按照插入顺序迭代;
TreeMap可以根据Key的自然顺序(如整数从小到大)或者自己定义的比较器,实现 Key 的有序排列。
map遍历
在java中所有的map都实现了Map接口,因此所有的Map(如HashMap, TreeMap, LinkedHashMap, Hashtable等)都可以用以下的方式去遍历。
方法一:在for循环中使用entries实现Map的遍历:
/**
* 最常见也是大多数情况下用的最多的,一般在键值对都需要使用
*/
Map <String,String>map = new HashMap<String,String>();
map.put("熊大", "棕色");
map.put("熊二", "黄色");
for(Map.Entry<String, String> entry : map.entrySet()){
String mapKey = entry.getKey();
String mapValue = entry.getValue();
System.out.println(mapKey+":"+mapValue);
}
方法二:在for循环中遍历key或者values,一般适用于只需要map中的key或者value时使用,在性能上比使用entrySet较好;
Map <String,String>map = new HashMap<String,String>();
map.put("熊大", "棕色");
map.put("熊二", "黄色");
//key
for(String key : map.keySet()){
System.out.println(key);
}
//value
for(String value : map.values()){
System.out.println(value);
}
方法三:通过Iterator遍历;
Iterator<Entry<String, String>> entries = map.entrySet().iterator();
while(entries.hasNext()){
Entry<String, String> entry = entries.next();
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key+":"+value);
}
方法四:通过键找值遍历,这种方式的效率比较低,因为本身从键取值是耗时的操作;
for(String key : map.keySet()){
String value = map.get(key);
System.out.println(key+":"+value);
}
list集合遍历
在集合中有三种遍历方式 for循环,迭代器,和foreach ,这三种在删除中方式一样吗?
ArrayList<Integer> array = new ArrayList<>();
array.add(1);
array.add(2);
array.add(2);
array.add(3);
//方式一
// for(int i = 0 ; i< array.size(); i++) {
// System.out.println(array.get(i));
// }
//方式二
// for(int i : array) {
// System.out.println(i);
// }
//方式三, 迭代器是没有索引的。
Iterator<Integer> iterator = array.iterator();
while( iterator.hasNext()) {
System.out.println(iterator.next());
}
}
list删除元素
总结:在只是遍历情况下,for循环,迭代器,和foerach 三个都可以用,在改变集合的情况下,不用第三个foreach
for循环方式
import java.util.ArrayList;
public class DeleteDemo {
public static void main(String[] args) {
ArrayList<Integer> array = new ArrayList<>();
array.add(1);
array.add(2);
array.add(2);
array.add(3);
for(int i=0;i<array.size();i++){
if(2==array.get(i)){
array.remove(i);
}
}
}}
结果:
注意
为什么 有一个2 没有被删除呢,当我们深究 for循环在集合中工作原理就可以理解,事实上,当删除一个元素时,后面元素的索引就会前进以为,如删除第一个2 时候,此时下一个元素2的索引就变成了1,而此时for循环执行i++,对应索引的位置为2 而新的1索引上的值就不会在读取了,所以结果为【1,2,3】,要解决这个问题很容易只需要把 array.remove(i);改为 array.remove(i);即可 即
import java.util.ArrayList;
public class DeleteDemo {
public static void main(String[] args) {
ArrayList<Integer> array = new ArrayList<>();
array.add(1);
array.add(2);
array.add(2);
array.add(3);
for(int i=0;i<array.size();i++){
if(2==array.get(i)){
array.remove(i--);
}
}
}}
输出结果为
iterator方式
介绍:
Java中的Iterator功能比较简单,并且只能单向移动:
(1) 使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。只有Colletion接口下的类才能使用迭代器。
(2) 使用next()获得序列中的下一个元素。
(3) 使用hasNext()检查序列中是否还有元素。
(4) 使用remove()将迭代器新返回的元素删除。
一次next,指针就往后移动一位。
警告:
1.如果集合是java.util包下面的类如:ArrayList、LinkedList
使用迭代器删除元素的时候,必须用iterator.remove()迭代器自己的删除方法删除,切记不可以使用collection集合的remove()方法删除。
原因:
util包下的集合类都是快速失败的,
2.如果集合是java.util.concurrent下的类如:ConcurrentHashMap, CopyOnWriteArrayList等默认为都是Fail-Safe。–这个包下面的类都是线程安全的类
那么在迭代删除的时候,是可以使用集合的删除方法的。但是不保证结果是正确的。所以并不是说安全失败不报异常,他就是好的。
Fail-Safe 迭代的缺点是:首先是iterator不能保证返回集合更新后的数据,因为其工作在集合克隆上,而非集合本身。其次,创建集合拷贝需要相应的开销,包括时间和内存。
无论快速失败还是安全失败都是不可取的,想要得到正确的结果就老老实实的使用迭代器的删除方法。
快速失败原理
当使用ArrayList.iterator()返回一个迭代器对象时。迭代器对象有一个属性expectedModCount,它被赋值为该方法调用时modCount的值。这意味着,这个值是modCount在这个时间点的快照值,expectedModCount值在iterator对象内部不会再发送变化!
这时候我们就能明白了,在得到迭代器之后,如果我们使用ArrayList的add、remove等方法,会使得modCount的值自增(发生了变化),而iterator内部的expectedModCount值却还是之前的快照值。我们再来看iterator的方法实现:可以看到,在调用next方法时,第一步就是检查modCount值和迭代器内部的expectedModCount值是否相等!显然,这是不等的,所以在调用next方法的时候,就抛出了ConcurrentModificationException异常。
安全失败原理
上面的fail-fast发生时,程序会抛出异常,而fail-safe是一个概念,并发容器的并发修改不会抛出异常,这和其实现有关。并发容器的iterate方法返回的iterator对象,内部都是保存了该集合对象的一个快照副本,并且没有modCount等数值做检查。如下图,这也造成了并发容器的iterator读取的数据是某个时间点的快照版本。你可以并发读取,不会抛出异常,但是不保证你遍历读取的值和当前集合对象的状态是一致的!这就是安全失败的含义。
如果:
如果你想在同一个迭代器既要删除指定的元素,又想打印剩下的元素
import java.util.ArrayList;
import java.util.Iterator;
public class DeleteDemo {
public static void main(String[] args) {
ArrayList<Integer> array = new ArrayList<>();
array.add(1);
array.add(2);
array.add(2);
array.add(3);
Iterator<Integer> it = array.iterator();
while (((Iterator) it).hasNext()){
if (2==it.next()) {
it.remove();
}
//这一步打印是不对的,因为两次next了
System.out.println(it.next());
}
结果是
分析:
2指的是第二个位置那个值,因为一次next到了1的位置,第二次next到了第二个位置。
抓住一次next指针就往后移动一位的原则,分析就行了。
为什么是这样呢,那是因为我们用了两次it.next() 那么要想满足条件该怎么办呢? 看了上次错误原因我们想只用一个it.next()就好了。此时修改下代码即可解决
import java.util.ArrayList;
import java.util.Iterator;
public class DeleteDemo {
public static void main(String[] args) {
ArrayList<Integer> array = new ArrayList<>();
array.add(1);
array.add(2);
array.add(2);
array.add(3);
Iterator<Integer> it = array.iterator();
while (((Iterator) it).hasNext()) {
int b = it.next();
if (2 != b) {
it.remove();
System.out.println(b);
}
}
}
}
但是这个方式不好,因为改变了我们的初衷,我们是想只删除集合里面指定的元素,这个方式却把我们没有指定的元素给删除了,虽然满足了遍历的需求,但是违背了初衷。
因此:
while (((Iterator) it).hasNext()) {
int b = it.next();
if (2 == b) {
it.remove();
}
}
// 此时已经把array集合里面的所有2元素删除了
//打印剩下的集合元素,, 结果是[1, 3]
System.out.println(array);
安全失败和快速失败
快速失败和安全失败是对集合迭代的过程中,对原集合进行增加、删除、修改发生的。
两个关键:1.集合迭代 2.使用原集合的增删改方法
多线程并发修改会触发安全失败或者快速失败如:迭代 器是一个线程,集合操作是另外一个线程,你在迭代遍历的时候,使用集合的remove去修改元素----> 一个线程正在遍历集合,另外一个线程去修改集合。
如果你使用迭代器的方式去remove,那么不涉及安全失败和快速失败,
在Collection集合的各个类中,有线程安全和线程不安全这2大类的版本。
对于线程不安全的类,并发情况下可能会出现fail-fast情况;而线程安全的类,可能出现fail-safe的情况。
快速失败:当你在迭代一个集合的时候,如果有另一个线程正在修改你正在访问的那个集合时,就会抛出一个ConcurrentModification异常。
这个另一个线程可以指集合操作(增加add或者删除remove),集合操作和迭代操作是两个不同的线程。
在java.util包下的都是快速失败。—因为uti包下面的类如ArrayList、LinkedList都是线程不安全的
场景:
当迭代ArrayList集合元素的时候,满足了某个条件就把这个元素删除如iterator == 2则删除。
(或者在迭代后,又使用了集合的add方法添加元素–> 都会造成快速失败)
如果你使用迭代器的remove方法删除则正确,
如果你使用了集合的remove方法删除会触发快速失败,报ConcurrentModification异常。因为集合操作的线程和迭代操作的线程不是同一个,属于并发操作。
安全失败:你在迭代的时候会去底层集合做一个拷贝,所以你在修改上层集合的时候是不会受影响的,不会抛出ConcurrentModification异常。
在java.util.concurrent包下的全是安全失败的。–concurrent包下面的集合类都是线程安全的,可以并发修改。
场景:
当迭代ArrayList集合元素的时候,满足了某个条件就把这个元素删除如iterator == 2则删除。
如果你使用迭代器的remove方法删除则正确,
如果你使用了集合的remove方法删除也可以,并且不会触发ConcurrentModification异常。
foreach方式
foreach是不能删除集合元素的。
foreach循环一种很方便的循环 但是没有索引 那么可以在集合中可以修改吗? 很遗憾不可以 我们用代码演示下
import java.util.ArrayList;
public class DeleteDemo {
public static void main(String[] args) {
ArrayList<Integer> array = new ArrayList<>();
array.add(1);
array.add(2);
array.add(2);
array.add(3);
for (Integer a:array) {
if (2==a){
array.remove(a);
}
System.out.println(a);
}
}
}
结果是
这个原因我们解释过 因为它自身不带删除方法,所以 这个不可以删除元素。
如何删除一些集合中满足条件的元素?
不能使用for或者foreach循环加list.remove()进行删除,因为使用remove方法删除后,集合的长度会变,导致一些元素被漏掉。
注意:
官方推荐使用迭代器进行删除list中的元素;
使用for或者foreach理论上是可以删除元素 的,但是每次删除完元素还需要重新计算集合元素个数。
1.使用外部迭代器删除集合中的所有奇数:
List<Integer> list = new ArrayList<>();
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer i = iterator.next();
if (i % 2 == 1) {
iterator.remove();
}
}
2.使用内部迭代器(流),删除集合中的所有奇数:
List<Integer> list = new ArrayList<>();
list = list.stream().
filter(x -> x % 2 == 0).
collect(Collectors.toList());
异常
return
场景一:
try {
System.out.println("try");
} catch (Exception e) {
System.out.println("catch");
}
finally {
// TODO: handle finally clause
System.out.println("finally");
}
> try
finally
场景二:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kg94ov6l-1588857152680)(img/TIM截图20200503214336.png)]
注意:
我们的test()方法是有返回值的,并且我们方法里面用到了 try 、catch、finally且均会返回一个值,那么在最外面就不应该再有一个返回值了如:20行,因为程序根本到不了20行。 如果非要20行有返回值,那么请把try 、catch、finally的返回全部去掉。
场景三:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9u6fcWvG-1588857152681)(img/TIM截图20200503215436.png)]
总结:
finally里面的代码一定要执行,
如果finally里面有return,那么直接返回finally里面的return内容。
多线程
创建线程有哪些方法(4种)?
继承Thread类,重写run方法(其实Thread类本身也实现了Runnable接口)
实现Runnable接口,重写run方法
实现Callable接口,重写call方法(有返回值)
使用线程池(有返回值)
Runnable和Callable有什么不同?
最大的不同点:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取结果;当不调用此方法时,主线程不会阻塞!
start()和run()方法有什么区别?
start()方法来启动线程,而启动后的线程运行的是run()方法中的代码。
run()方法当作普通方法的方式调用时,并不会开启新的线程。
在具体多线程编程实践中,如何选用Runnable还是Thread?
Java中实现多线程有两种方法:继承Thread类、实现Runnable接口,在程序开发中只要是多线程,**肯定永远以实现Runnable接口为主,**因为实现Runnable接口相比继承Thread类有如下优势:
1、可以避免由于Java的单继承特性而带来的局限;
2、增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;
线程池有哪些参数
最常用的三个参数:
corePoolSize:核心线程数
queueCapacity:任务队列容量(阻塞队列)
maxPoolSize:最大线程数
三个参数的作用:
当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满
若线程数小于最大线程数,创建线程
若线程数等于最大线程数,抛出异常,拒绝任务
线程池有几种
ExecutorService threadPool = null;
threadPool = Executors.newCachedThreadPool();//有缓冲的线程池,线程数 JVM 控制
threadPool = Executors.newFixedThreadPool(3);//固定大小的线程池
threadPool = Executors.newScheduledThreadPool(2);//一个能实现定时、周期性任务的线程池
threadPool = Executors.newSingleThreadExecutor();//单线程的线程池,只有一个线程在工作
volatile底层是怎么实现的?
当一个变量定义为volatile
后,它将具备两种特性:1. 可见性,2. 禁止指令重排序。
可见性:保证线程获取的被volatile修饰的变量的值是最新值
volatile与synchronized有什么区别?
volatile仅能使用在变量上,synchronized则可以使用在方法、类、同步代码块等等。
volatile只能保证可见性和有序性,不能保证原子性。而synchronized都可以保证。
volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
java中的线程有几种状态
创建
runable
runniing
block: wait进入等待池,同步方法、同步代码块进入等锁池,sleep、join线程休眠。
dead
JAVA动态代理
源于Java设计模式中的代理模式。
在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。
如果是纯代理模式的案例,那么代理对象WanMeiLiProxy实现的是Gril接口。
为什么使用代理模式(什么场景会使用代理模式)
添加日志、判断权限、事务、、、、、、
JDK动态代理
代理的是接口。
1.接口
public interface Girl {
void date();
void watchMovie();
}
2.真实对象
public class WangMeiLi implements Girl{
@Override
public void date() {
// TODO Auto-generated method stub
System.out.println("我是王美丽");
}
@Override
public void watchMovie() {
// TODO Auto-generated method stub
System.out.println("喜欢看电影");
}
}
3.代理对象(处理器对象)
public class WanMeiLiProxy implements InvocationHandler{
Girl girl;
public WanMeiLiProxy(Girl girl) {
// TODO Auto-generated constructor stub
this.girl = girl;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("前置增强");
//用到了反射机制
method.invoke(girl, args);
System.out.println("后置增强");
return null;
}
}
4.调用端
public class ProxyTest {
public static void main(String[] args) {
//被代理对象
WangMeiLi wangMeiLi = new WangMeiLi();
//代理类
WanMeiLiProxy wanMeiLiProxy = new WanMeiLiProxy(wangMeiLi);
//得到代理对象
Girl girl = (Girl) Proxy.newProxyInstance(wangMeiLi.getClass().getClassLoader(), wangMeiLi.getClass().getInterfaces(), wanMeiLiProxy);
girl.date();
}
}
Cglib动态代理
Cglib动态代理是针对代理的类, 动态生成一个子类, 然后子类覆盖代理类中的方法, 如果是private或是final类修饰的方法,则不会被重写。
CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。通常可以使用Java的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB是一个好的选择。
MySql
sql优化
1.sql优化主要是对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引
2.任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
3.避免在 where 子句中使用**!=或<>**操作符,否则数据库引擎放弃使用索引而进行全表扫描,
4.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描。可以在 null上设置默认值,确保表中没有null值。
5.复合索引必须指定索引中第一个字段作为检索条件,否则不使用索引
6.in 和 not in 也要慎用,否则会导致全表扫描。对于连续的数值,能用 between 就不要用 in 了。
7.模糊查询时,以"%"开头,会导致全表扫描
索引
什么是索引
索引(Index)是帮助MySQL高效获取数据的数据结构,可以理解为一本字典的目录,提高程序的检索 和查询效率。
b+树:
索引的类型
主键索引:数据列不允许重复,不允许为NULL,一个表只能有一个主键。
唯一索引:数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。
#创建唯一索引
ALTER TABLE table_name ADD UNIQUE (column);
#创建唯一组合索引
ALTER TABLE table_name ADD UNIQUE (column1,column2);
普通索引:基本的索引类型,没有唯一性的限制,允许为NULL值。
#创建普通索引
ALTER TABLE table_name ADD INDEX index_name (column);
#创建组合索引
ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);
全文索引:是目前搜索引擎使用的一种关键技术。
#创建全文索引
ALTER TABLE table_name ADD FULLTEXT (column);
事务
Mysql事务的隔离级别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uh9xIs95-1588857152682)(img/TIM截图20200501151818.png)]
什么是脏读?什么是幻读?
脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。事务1在两次查询的过程中,事务2对该表进行了插入、删除操作,从而事务1第二次查询的结果发生了变化。
总结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
存储引擎
MyIsam:
查询快,不支持事务,不支持外键
INNODB:(MySQL数据库的默认引擎)
查询慢,提供事务,提供外键
如何选择合适的存储引擎
1.MyISAM表最适合于大量的数据读而少量数据更新的混合操作。MyISAM表的另一种适用情形是使用压缩的中读表。
2.如果查询中包含较多的数据更新操作,应使用InnoDB。其行级锁机制和多版本的支持为数据读取和更新的混合提供了良好的并发机制。
3.可使用MEMORY存储引擎存储非永久需要的数据,或者是能够从基于磁盘的表中重新生成的数据。
Innodb的事务锁有哪些?(4种)
行级锁:
共享锁(S Lock) : 允许事务读一行数据
排它锁(X Lock) : 允许事务删除或更新一行数据
表级锁:
意向共享锁(IS Lock):事务想要获得一张表中某几行的共享锁
意向排它锁(IX Lock):事务想要获得一张表中某几行的排它锁
由于Innodb引擎支持的均为行锁,所以意向锁其实不会阻塞除全表扫描之外的任何请求
数据库的乐观锁怎么实现的?
**乐观锁:**假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
实现方式:
在数据库中加入一个版本号字段,每次在执行数据的修改操作时,先查出相应的版本号,然后带上查出来的这个版本号去修改数据库,一旦该版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败
上述方法中的版本号也可以用时间戳替换,时间戳不需要 +1 操作。
使用场景:
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
表结构是怎么设计的?
首先,表结构的设计尽量要满足数据库三范式
但是还是根据实际需求进行取舍,有时可能会拿冗余换速度,最终用目的要满足客户需求。
第一范式:每一个字段是原子性不能再分;
第二范式:在第一范式基础之上,要求数据库中所有非主键字段完全依赖主键,不能产生部分依赖;
第三范式:在第二范式基础之上,要求非主键字段不能产生传递依赖于主键字段
如何防止SQL注入?
SQL注入产生的原因,就是用户提交的数据,被数据库系统编译而产生了开发者预期之外的动作。也就是,SQL注入是用户输入的数据,在拼接SQL语句的过程中,超越了数据本身,成为了SQL语句查询逻辑的一部分,然后这样被拼接出来的SQL语句被数据库执行,产生了开发者预期之外的动作。
解决办法:
1.最简单的就是采用预编译语句集(PreparedStatement),在SQL语句中放置占位符’?’,在执行前,数据库会先对含有占位符的SQL语句进行校验和预编译;执行时才会把参数传递进去,防止参数参与编译变成sql语句的一部分。
2.采用正则表达式将包含有 单引号(’),分号(😉 和 注释符号(–)的语句给替换掉来防止SQL注入
框架
计算机网络
OSI七层模型
TCP/IP四层模型
JAVA实现TCP通信
协议相当于相互通信的程序间达成的一种约定,它规定了分组报文的结构、交换方式、包含的意义以及怎样对报文所包含的信息进行解析。
TCP协议提供面向连接的服务,通过它建立的是可靠地连接。Java为TCP协议提供了两个类:Socket类和ServerSocket类。一个Socket实例代表了TCP连接的一个客户端,而一个ServerSocket实例代表了TCP连接的一个服务器端。
服务器端的ServerSocket实例则监听来自客户端的TCP连接请求,并为每个请求创建新的Socket实例,由于服务端在调用accept()等待客户端的连接请求时会阻塞,直到收到客户端发送的连接请求才会继续往下执行代码,因此要为每个Socket连接开启一个线程。服务器端要同时处理ServerSocket实例和Socket实例,而客户端只需要使用Socket实例。
另外,每个Socket实例会关联一个InputStream和OutputStream对象,我们通过将字节写入套接字的OutputStream来发送数据,并通过从InputStream来接收数据。
HTTP与TCP的区别和联系
TCP就是单纯建立连接,不涉及任何我们需要请求的实际数据,简单的传输。http是用来收发数据,即实际应用上来的。
TCP是底层通讯协议,定义的是数据传输和连接方式的规范
HTTP是应用层协议,定义的是传输数据的内容的规范
HTTP协议中的数据是利用TCP协议传输的,所以支持HTTP也就一定支持TCP