java面试题及答案
2021/7/6 1:28:22
本文主要是介绍java面试题及答案,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
java基础面试题
JDK与JRE?
JDK:Java Development Kit 的简称,Java 开发工具包,提供了 Java 的开发环境和运行环境。 JRE:Java Runtime Environment 的简称,Java 运行环境,为 Java 的运行提供了所需环境。 具体来说 JDK 其实包含了 JRE,同时还包含了编译 Java 源码的编译器 Javac,还包含了很多 Java 程序调试和分析的工具。简单来说:如果你需要运行 Java 程序,只需安装 JRE 就可以了,如果你需要编写 Java 程序,需要安装 JDK。
==和equals:
对于基本类型和引用类型 == 的作用效果是不同的,如下所示:
基本类型:比较的是值是否相同; 引用类型:比较的是引用是否相同;
equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法
== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。
很显然“通话”和“重地”的 hashCode() 相同,然而 equals() 则为 false,因为在散列表中,hashCode()相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。
final关键字:
final 修饰的类叫最终类,该类不能被继承。
final 修饰的方法不能被重写。
final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。
操作字符串的StringBuilder,StringBuffer
操作字符串的类有:String、StringBuffer、StringBuilder。
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。
String str=”i”与 String str=new String(“i”)一样吗?
不一样,因为内存的分配方式不一样。String str=”i”的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中。
如何将字符串反转?
使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
String 类的常用方法都有那些?
indexOf():返回指定字符的索引。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。
抽象类必须要有抽象方法吗?
不需要,抽象类不一定非要有抽象方法。
普通类和抽象类有哪些区别?
普通类不能包含抽象方法,抽象类可以包含抽象方法。
抽象类不能直接实例化,普通类可以直接实例化。
抽象类能使用 final 修饰吗?
不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类,如下图所示,编辑器也会提示错误信息:
接口和抽象类有什么区别?
实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
构造函数:抽象类可以有构造函数;接口不能有。
main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。
实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。
java 中 IO 流分为几种?
按功能来分:输入流(input)、输出流(output)。
按类型来分:字节流和字符流。
字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。
BIO、NIO、AIO 有什么区别?
BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
Files的常用方法都有哪些?
Files.exists():检测文件路径是否存在。
Files.createFile():创建文件。
Files.createDirectory():创建文件夹。
Files.delete():删除一个文件或目录。
Files.copy():复制文件。
Files.move():移动文件。
Files.size():查看文件个数。
Files.read():读取文件。
Files.write():写入文件。
Collection 和 Collections 有什么区别?
java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。
List、Set、Map 之间的区别是什么?
HashMap 和 Hashtable 有什么区别?
hashMap去掉了HashTable 的contains方法,但是加上了containsValue()和containsKey()方法。
hashTable同步的,而HashMap是非同步的,效率上逼hashTable要高。
hashMap允许空键值,而hashTable不允许。
如何决定使用 HashMap 还是 TreeMap?
对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。
说一下 HashMap 的实现原理?
HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap的数据结构: 在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
当我们往Hashmap中put元素时,首先根据key的hashcode重新计算hash值,根绝hash值得到这个元素在数组中的位置(下标),如果该数组在该位置上已经存放了其他元素,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放入链尾.如果数组中该位置没有元素,就直接将该元素放到数组的该位置上。
需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)
说一下 HashSet 的实现原理?
HashSet底层由HashMap实现
HashSet的值存放于HashMap的key上
HashMap的value统一为PRESENT
ArrayList 和 LinkedList 的区别是什么?
最明显的区别是 ArrrayList底层的数据结构是数组,支持随机访问,而 LinkedList 的底层数据结构是双向循环链表,不支持随机访问。使用下标访问一个元素,ArrayList 的时间复杂度是 O(1),而 LinkedList 是 O(n)。
如何实现数组和 List 之间的转换?
List转换成为数组:调用ArrayList的toArray方法。
数组转换成为List:调用Arrays的asList方法。
ArrayList 和 Vector 的区别是什么?
Vector是同步的,而ArrayList不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用CopyOnWriteArrayList。
ArrayList比Vector快,它因为有同步,不会过载。
ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。
Array 和 ArrayList 有何区别?
Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
Array是指定大小的,而ArrayList大小是固定的。
Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。
在 Queue 中 poll()和 remove()有什么区别?
poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。
哪些集合类是线程安全的?
vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
statck:堆栈类,先进后出。
hashtable:就比hashmap多了个线程安全。
enumeration:枚举,相当于迭代器。
迭代器 Iterator 是什么?
迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象,因为创建它的代价小。
Iterator 怎么使用?有什么特点?
Java中的Iterator功能比较简单,并且只能单向移动:
(1) 使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。
(2) 使用next()获得序列中的下一个元素。
(3) 使用hasNext()检查序列中是否还有元素。
(4) 使用remove()将迭代器新返回的元素删除。
Iterator是Java迭代器最简单的实现,为List设计的ListIterator具有更多的功能,它可以从两个方向遍历List,也可以从List中插入和删除元素。
Iterator 和 ListIterator 有什么区别?
Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。
Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。
多线程
并行和并发有什么区别?
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
线程和进程的区别?
简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
守护线程是什么?
守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程。
创建线程有哪几种方式?
①. 继承Thread类创建线程类
-
定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
-
创建Thread子类的实例,即创建了线程对象。
-
调用线程对象的start()方法来启动该线程。
②. 通过Runnable接口创建线程类
-
定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
-
创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
-
调用线程对象的start()方法来启动该线程。
③. 通过Callable和Future创建线程
-
创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
-
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
-
使用FutureTask对象作为Thread对象的target创建并启动新线程。
-
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
-
说一下 runnable 和 callable 有什么区别?
有点深的问题了,也看出一个Java程序员学习知识的广度。
-
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
-
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
线程有哪些状态?
线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。
-
创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
-
就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
-
运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
-
阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
-
死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪
sleep() 和 wait() 有什么区别?
sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程
notify()和 notifyAll()有什么区别?
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
线程的 run()和 start()有什么区别?
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程。
start()方法来启动一个线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
创建线程池有哪几种方式?
①. newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
②. newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
③. newSingleThreadExecutor()
这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
④. newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
线程池都有哪些状态?
线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。
线程池各个状态切换框架图:
线程池中 submit()和 execute()方法有什么区别?
接收的参数不一样
submit有返回值,而execute没有
submit方便Exception处理
在 java 程序中怎么保证多线程的运行安全?
线程安全在三个方面体现:
-
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
-
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
-
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
多线程锁的升级原理是什么?
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
锁升级的图示过程:
什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。
怎么防止死锁?
死锁的四个必要条件:
-
互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
-
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
-
不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
-
环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁。
所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确 定资源的合理分配算法,避免进程永久占据系统资源。
此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。
ThreadLocal 是什么?有哪些使用场景?
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
说一下 synchronized 底层实现原理?
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
-
普通同步方法,锁是当前实例对象
-
静态同步方法,锁是当前类的class对象
-
同步方法块,锁是括号里面的对象
synchronized 和 volatile 的区别是什么?
-
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
-
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
-
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
-
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
-
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
synchronized 和 Lock 有什么区别?
-
首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
-
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
-
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
-
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
-
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
-
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
synchronized 和 ReentrantLock 区别是什么?
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
-
ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
-
ReentrantLock可以获取各种锁的信息
-
ReentrantLock可以灵活地实现多路通知
另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。
说一下 atomic 的原理?
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。
反射
什么是反射?
反射主要是指程序可以访问、检测和修改它本身状态或行为的一种能力
Java反射
在Java运行时环境中,对于任意一个类,能否知道这个类有哪些属性和方法?对于任意一个对象,能否调用它的任意一个方法
Java反射机制主要提供了以下功能:
-
在运行时判断任意一个对象所属的类。
-
在运行时构造任意一个类的对象。
-
在运行时判断任意一个类所具有的成员变量和方法。
-
在运行时调用任意一个对象的方法。
什么是 java 序列化?什么情况下需要序列化?
简单说就是为了保存在内存中的各种对象的状态(也就是实例变量,不是方法),并且可以把保存的对象状态再读出来。虽然你可以用你自己的各种各样的方法来保存object states,但是Java给你提供一种应该比你自己好的保存对象状态的机制,那就是序列化。
什么情况下需要序列化:
a)当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
b)当你想用套接字在网络上传送对象的时候;
c)当你想通过RMI传输对象的时候;
动态代理是什么?有哪些应用?
动态代理:
当想要给实现了某个接口的类中的方法,加一些额外的处理。比如说加日志,加事务等。可以给这个类创建一个代理,故名思议就是创建一个新的类,这个类不仅包含原来类方法的功能,而且还在原来的基础上添加了额外处理的新类。这个代理类并不是定义好的,是动态生成的。具有解耦意义,灵活,扩展性强。
动态代理的应用:
-
Spring的AOP
-
加事务
-
加权限
-
加日志
怎么实现动态代理?
首先必须定义一个接口,还要有一个InvocationHandler(将实现接口的类的对象传递给它)处理类。再有一个工具类Proxy(习惯性将其称为代理类,因为调用他的newInstance()可以产生代理对象,其实他只是一个产生代理对象的工具类)。利用到InvocationHandler,拼接代理类源码,将其编译生成代理类的二进制码,利用加载器加载,并将其实例化产生代理对象,最后返回。
对象拷贝
为什么要使用克隆?
想对一个对象进行处理,又想保留原有的数据进行接下来的操作,就需要克隆了,Java语言中克隆针对的是类的实例。
如何实现对象克隆?
有两种方式:
1). 实现Cloneable接口并重写Object类中的clone()方法;
jsp 和 servlet 有什么区别?
-
jsp经编译后就变成了Servlet.(JSP的本质就是Servlet,JVM只能识别java的类,不能识别JSP的代码,Web容器将JSP的代码编译成JVM能够识别的java类)
-
jsp更擅长表现于页面显示,servlet更擅长于逻辑控制。
-
Servlet中没有内置对象,Jsp中的内置对象都是必须通过HttpServletRequest对象,HttpServletResponse对象以及HttpServlet对象得到。
-
Jsp是Servlet的一种简化,使用Jsp只需要完成程序员需要输出到客户端的内容,Jsp中的Java脚本如何镶嵌到一个类中,由Jsp容器完成。而Servlet则是个完整的Java类,这个类的Service方法用于生成对客户端的响应。
-
jsp 有哪些内置对象?作用分别是什么?
JSP有9个内置对象:
-
request:封装客户端的请求,其中包含来自GET或POST请求的参数;
-
response:封装服务器对客户端的响应;
-
pageContext:通过该对象可以获取其他对象;
-
session:封装用户会话的对象;
-
application:封装服务器运行环境的对象;
-
out:输出服务器响应的输出流对象;
-
config:Web应用的配置对象;
-
page:JSP页面本身(相当于Java程序中的this);
-
exception:封装页面抛出异常的对象。
说一下 jsp 的 4 种作用域?
JSP中的四种作用域包括page、request、session和application,具体来说:
-
page代表与一个页面相关的对象和属性。
-
request代表与Web客户机发出的一个请求相关的对象和属性。一个请求可能跨越多个页面,涉及多个Web组件;需要在页面显示的临时数据可以置于此作用域。
-
session代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的session中。
-
application代表与整个Web应用程序相关的对象和属性,它实质上是跨越整个Web应用程序,包括多个页面、请求和会话的一个全局作用域。
session 和 cookie 有什么区别?
-
由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session.典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session。
-
思考一下服务端如何识别特定的客户?这个时候Cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。
-
Cookie其实还可以用在一些方便用户的场景下,设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了,怎么办?这个信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。这也是Cookie名称的由来,给用户的一点甜头。所以,总结一下:Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。
说一下 session 的工作原理?
其实session是一个存在服务器上的类似于一个散列表格的文件。里面存有我们需要的信息,在我们需要用的时候可以从里面取出来。类似于一个大号的map吧,里面的键存储的是用户的sessionid,用户向服务器发送请求的时候会带上这个sessionid。这时就可以从中取出对应的值了。
如果客户端禁止 cookie 能实现 session 还能用吗?
Cookie与 Session,一般认为是两个独立的东西,Session采用的是在服务器端保持状态的方案,而Cookie采用的是在客户端保持状态的方案。但为什么禁用Cookie就不能得到Session呢?因为Session是用Session ID来确定当前对话所对应的服务器Session,而Session ID是通过Cookie来传递的,禁用Cookie相当于失去了Session ID,也就得不到Session了。
假定用户关闭Cookie的情况下使用Session,其实现途径有以下几种:
-
设置php.ini配置文件中的“session.use_trans_sid = 1”,或者编译时打开打开了“—enable-trans-sid”选项,让PHP自动跨页传递Session ID。
-
手动通过URL传值、隐藏表单传递Session ID。
-
用文件、数据库等形式保存Session ID,在跨页过程中手动调用。
-
spring mvc 和 struts 的区别是什么?
-
拦截机制的不同
Struts2是类级别的拦截,每次请求就会创建一个Action,和Spring整合时Struts2的ActionBean注入作用域是原型模式prototype,然后通过setter,getter吧request数据注入到属性。Struts2中,一个Action对应一个request,response上下文,在接收参数时,可以通过属性接收,这说明属性参数是让多个方法共享的。Struts2中Action的一个方法可以对应一个url,而其类属性却被所有方法共享,这也就无法用注解或其他方式标识其所属方法了,只能设计为多例。
SpringMVC是方法级别的拦截,一个方法对应一个Request上下文,所以方法直接基本上是独立的,独享request,response数据。而每个方法同时又何一个url对应,参数的传递是直接注入到方法中的,是方法所独有的。处理结果通过ModeMap返回给框架。在Spring整合时,SpringMVC的Controller Bean默认单例模式Singleton,所以默认对所有的请求,只会创建一个Controller,有应为没有共享的属性,所以是线程安全的,如果要改变默认的作用域,需要添加@Scope注解修改。
Struts2有自己的拦截Interceptor机制,SpringMVC这是用的是独立的Aop方式,这样导致Struts2的配置文件量还是比SpringMVC大。
-
低层架构的不同
Struts2采用Filter(StrutsPrepareAndExecuteFilter)实现,SpringMVC(DispatcherServlet)则采用Servlet实现。Filter在容器启动之后即初始化;服务停止以后坠毁,晚于Servlet。Servlet在是在调用时初始化,先于Filter调用,服务停止后销毁。
-
性能方面
Struts2是类级别的拦截,每次请求对应实例一个新的Action,需要加载所有的属性值注入,SpringMVC实现了零配置,由于SpringMVC基于方法的拦截,有加载一次单例模式bean注入。所以,SpringMVC开发效率和性能高于Struts2。
-
配置方面
spring MVC和Spring是无缝的。从这个项目的管理和安全上也比Struts2高。
如何避免 sql 注入?
-
PreparedStatement(简单又有效的方法)
-
使用正则表达式过滤传入的参数
-
字符串过滤
-
JSP中调用该函数检查是否包函非法字符
-
JSP页面判断代码
-
什么是 XSS 攻击,如何避免?
XSS攻击又称CSS,全称Cross Site Script (跨站脚本攻击),其原理是攻击者向有XSS漏洞的网站中输入恶意的 HTML 代码,当用户浏览该网站时,这段 HTML 代码会自动执行,从而达到攻击的目的。XSS 攻击类似于 SQL 注入攻击,SQL注入攻击中以SQL语句作为用户输入,从而达到查询/修改/删除数据的目的,而在xss攻击中,通过插入恶意脚本,实现对用户游览器的控制,获取用户的一些信息。 XSS是 Web 程序中常见的漏洞,XSS 属于被动式且用于客户端的攻击方式。
XSS防范的总体思路是:对输入(和URL参数)进行过滤,对输出进行编码。
throw 和 throws 的区别?
throws是用来声明一个方法可能抛出的所有异常信息,throws是将异常声明但是不处理,而是将异常往上传,谁调用我就交给谁处理。而throw则是指抛出的一个具体的异常类型。
final、finally、finalize 有什么区别?
-
final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
-
finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
-
finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System的gc()方法的时候,由垃圾回收器调用finalize(),回收垃圾。
try-catch-finally 中哪个部分可以省略?
答:catch可以省略
原因:
更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。
理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上try,代码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理,或者加上catch捕获以便进一步处理。
至于加上finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。
try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?
答:会执行,在 return 前执行。
常见的异常类有哪些?
-
NullPointerException:当应用程序试图访问空对象时,则抛出该异常。
-
SQLException:提供关于数据库访问错误或其他错误信息的异常。
-
IndexOutOfBoundsException:指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。
-
NumberFormatException:当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
-
FileNotFoundException:当试图打开指定路径名表示的文件失败时,抛出此异常。
-
IOException:当发生某种I/O异常时,抛出此异常。此类是失败或中断的I/O操作生成的异常的通用类。
-
ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出该异常。
-
ArrayStoreException:试图将错误类型的对象存储到一个对象数组时抛出的异常。
-
IllegalArgumentException:抛出的异常表明向方法传递了一个不合法或不正确的参数。
-
ArithmeticException:当出现异常的运算条件时,抛出此异常。例如,一个整数“除以零”时,抛出此类的一个实例。
-
NegativeArraySizeException:如果应用程序试图创建大小为负的数组,则抛出该异常。
-
NoSuchMethodException:无法找到某一特定方法时,抛出该异常。
-
SecurityException:由安全管理器抛出的异常,指示存在安全侵犯。
-
UnsupportedOperationException:当不支持请求的操作时,抛出该异常。
-
RuntimeExceptionRuntimeException:是那些可能在Java虚拟机正常运行期间抛出的异常的超类。
forward 和 redirect 的区别?
Forward和Redirect代表了两种请求转发方式:直接转发和间接转发。
直接转发方式(Forward),客户端和浏览器只发出一次请求,Servlet、HTML、JSP或其它信息资源,由第二个信息资源响应该请求,在请求对象request中,保存的对象对于每个信息资源是共享的。
间接转发方式(Redirect)实际是两次HTTP请求,服务器端在响应第一次请求的时候,让浏览器再向另外一个URL发出请求,从而达到转发的目的。
举个通俗的例子:
直接转发就相当于:“A找B借钱,B说没有,B去找C借,借到借不到都会把消息传递给A”;
间接转发就相当于:”A找B借钱,B说没有,让A去找C借”。
简述 tcp 和 udp的区别?
-
TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。
-
TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。
-
Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。
-
UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。
-
每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。
-
TCP对系统资源要求较多,UDP对系统资源要求较少。
tcp 为什么要三次握手,两次不行吗?为什么?
为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤。
如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认。
OSI 的七层模型都有哪些?
1.应用层:网络服务与最终用户的一个接口。
2.表示层:数据的表示、安全、压缩。
3.会话层:建立、管理、终止会话。
4.传输层:定义传输数据的协议端口号,以及流控和差错校验。
5.网络层:进行逻辑地址寻址,实现不同网络之间的路径选择。
6.数据链路层:建立逻辑连接、进行硬件地址寻址、差错校验等功能。
7.物理层:建立、维护、断开物理连接。
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中。
如何实现跨域?
方式一:图片ping或script标签跨域
图片ping常用于跟踪用户点击页面或动态广告曝光次数。
script标签可以得到从其他来源数据,这也是JSONP依赖的根据。
方式二:JSONP跨域
JSONP(JSON with Padding)是数据格式JSON的一种“使用模式”,可以让网页从别的网域要数据。根据 XmlHttpRequest 对象受到同源策略的影响,而利用 <script>元素的这个开放策略,网页可以得到从其他来源动态产生的JSON数据,而这种使用模式就是所谓的 JSONP。用JSONP抓到的数据并不是JSON,而是任意的JavaScript,用 JavaScript解释器运行而不是用JSON解析器解析。所有,通过Chrome查看所有JSONP发送的Get请求都是js类型,而非XHR。
缺点:
-
只能使用Get请求
-
不能注册success、error等事件监听函数,不能很容易的确定JSONP请求是否失败
-
JSONP是从其他域中加载代码执行,容易受到跨站请求伪造的攻击,其安全性无法确保
方式三:CORS
Cross-Origin Resource Sharing(CORS)跨域资源共享是一份浏览器技术的规范,提供了 Web 服务从不同域传来沙盒脚本的方法,以避开浏览器的同源策略,确保安全的跨域数据传输。现代浏览器使用CORS在API容器如XMLHttpRequest来减少HTTP请求的风险来源。与 JSONP 不同,CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。服务器一般需要增加如下响应头的一种或几种:
Spring
为什么要使用 spring?
1.简介
-
目的:解决企业应用开发的复杂性
-
功能:使用基本的JavaBean代替EJB,并提供了更多的企业应用功能
-
范围:任何Java应用
简单来说,Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。
2.轻量
从大小与开销两方面而言Spring都是轻量的。完整的Spring框架可以在一个大小只有1MB多的JAR文件里发布。并且Spring所需的处理开销也是微不足道的。此外,Spring是非侵入式的:典型地,Spring应用中的对象不依赖于Spring的特定类。
3.控制反转
Spring通过一种称作控制反转(IoC)的技术促进了松耦合。当应用了IoC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。你可以认为IoC与JNDI相反——不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。
4.面向切面
Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。
5.容器
Spring包含并管理应用对象的配置和生命周期,在这个意义上它是一种容器,你可以配置你的每个bean如何被创建——基于一个可配置原型(prototype),你的bean可以创建一个单独的实例或者每次需要时都生成一个新的实例——以及它们是如何相互关联的。然而,Spring不应该被混同于传统的重量级的EJB容器,它们经常是庞大与笨重的,难以使用。
6.框架
Spring可以将简单的组件配置、组合成为复杂的应用。在Spring中,应用对象被声明式地组合,典型地是在一个XML文件里。Spring也提供了很多基础功能(事务管理、持久化框架集成等等),将应用逻辑的开发留给了你。
所有Spring的这些特征使你能够编写更干净、更可管理、并且更易于测试的代码。它们也为Spring中的各种模块提供了基础支持。
解释一下什么是 aop?
AOP(Aspect-Oriented Programming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。
使用“横切”技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。Aop 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。正如Avanade公司的高级方案构架师Adam Magee所说,AOP的核心思想就是“将应用程序中的商业逻辑同对其提供支持的通用服务进行分离。”
解释一下什么是 ioc?
IOC是Inversion of Control的缩写,多数书籍翻译成“控制反转”。
简单来说就是把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。
IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。如下图:
spring 有哪些主要模块?
Spring框架至今已集成了20多个模块。这些模块主要被分如下图所示的核心容器、数据访问/集成,、Web、AOP(面向切面编程)、工具、消息和测试模块。
spring 常用的注入方式有哪些?
Spring通过DI(依赖注入)实现IOC(控制反转),常用的注入方式主要有三种:
构造方法注入
setter注入
基于注解的注入
spring 中的 bean 是线程安全的吗?
Spring容器中的Bean是否线程安全,容器本身并没有提供Bean的线程安全策略,因此可以说spring容器中的Bean本身不具备线程安全的特性,但是具体还是要结合具体scope的Bean去研究。
spring 支持几种 bean 的作用域?
当通过spring容器创建一个Bean实例时,不仅可以完成Bean实例的实例化,还可以为Bean指定特定的作用域。Spring支持如下5种作用域:
-
singleton:单例模式,在整个Spring IoC容器中,使用singleton定义的Bean将只有一个实例
-
prototype:原型模式,每次通过容器的getBean方法获取prototype定义的Bean时,都将产生一个新的Bean实例
-
request:对于每次HTTP请求,使用request定义的Bean都将产生一个新实例,即每次HTTP请求将会产生不同的Bean实例。只有在Web应用中使用Spring时,该作用域才有效
-
session:对于每次HTTP Session,使用session定义的Bean豆浆产生一个新实例。同样只有在Web应用中使用Spring时,该作用域才有效
-
globalsession:每个全局的HTTP Session,使用session定义的Bean都将产生一个新实例。典型情况下,仅在使用portlet context的时候有效。同样只有在Web应用中使用Spring时,该作用域才有效
其中比较常用的是singleton和prototype两种作用域。对于singleton作用域的Bean,每次请求该Bean都将获得相同的实例。容器负责跟踪Bean实例的状态,负责维护Bean实例的生命周期行为;如果一个Bean被设置成prototype作用域,程序每次请求该id的Bean,Spring都会新建一个Bean实例,然后返回给程序。在这种情况下,Spring容器仅仅使用new 关键字创建Bean实例,一旦创建成功,容器不在跟踪实例,也不会维护Bean实例的状态。
如果不指定Bean的作用域,Spring默认使用singleton作用域。Java在创建Java实例时,需要进行内存申请;销毁实例时,需要完成垃圾回收,这些工作都会导致系统开销的增加。因此,prototype作用域Bean的创建、销毁代价比较大。而singleton作用域的Bean实例一旦创建成功,可以重复使用。因此,除非必要,否则尽量避免将Bean被设置成prototype作用域。
spring 自动装配 bean 有哪些方式?
Spring容器负责创建应用程序中的bean同时通过ID来协调这些对象之间的关系。作为开发人员,我们需要告诉Spring要创建哪些bean并且如何将其装配到一起。
spring中bean装配有两种方式:
-
隐式的bean发现机制和自动装配
-
在java代码或者XML中进行显示配置
当然这些方式也可以配合使用。
spring 事务实现方式有哪些?
编程式事务管理对基于 POJO 的应用来说是唯一选择。我们需要在代码中调用beginTransaction()、commit()、rollback()等事务管理相关的方法,这就是编程式事务管理。
基于 TransactionProxyFactoryBean 的声明式事务管理
基于 @Transactional 的声明式事务管理
基于 Aspectj AOP 配置事务
说一下 spring 的事务隔离?
事务隔离级别指的是一个事务对数据的修改与另一个并行的事务的隔离程度,当多个事务同时访问相同数据时,如果没有采取必要的隔离机制,就可能发生以下问题:
-
脏读:一个事务读到另一个事务未提交的更新数据。
-
幻读:例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样。
-
不可重复读:比方说在同一个事务中先后执行两条一模一样的select语句,期间在此次事务中没有执行过任何DDL语句,但先后得到的结果不一致,这就是不可重复读。
SpringMVC
Spring运行流程描述:
\1. 用户向服务器发送请求,请求被Spring 前端控制Servelt DispatcherServlet捕获;
-
DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI)。然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain对象的形式返回;
-
DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter;(附注:如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler(…)方法)
\4. 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。 在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:
-
HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息
-
数据转换:对请求消息进行数据转换。如String转换成Integer、Double等
-
数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
-
数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中
\5. Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象;
\6. 根据返回的ModelAndView,选择一个适合的ViewResolver(必须是已经注册到Spring容器中的ViewResolver)返回给DispatcherServlet ;
-
ViewResolver 结合Model和View,来渲染视图;
\8. 将渲染结果返回给客户端。
spring mvc 有哪些组件?
Spring MVC的核心组件:
DispatcherServlet:中央控制器,把请求给转发到具体的控制类
Controller:具体处理请求的控制器
HandlerMapping:映射处理器,负责映射中央处理器转发给controller时的映射策略
ModelAndView:服务层返回的数据和视图层的封装类
ViewResolver:视图解析器,解析具体的视图
Interceptors :拦截器,负责拦截我们定义的请求然后做处理工作
@RequestMapping 的作用是什么?
RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。
RequestMapping注解有六个属性,下面我们把她分成三类进行说明。
value, method:
-
value:指定请求的实际地址,指定的地址可以是URI Template 模式(后面将会说明);
-
method:指定请求的method类型, GET、POST、PUT、DELETE等;
consumes,produces
-
consumes:指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;
-
produces:指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回;
params,headers
-
params: 指定request中必须包含某些参数值是,才让该方法处理。
-
headers:指定request中必须包含某些指定的header值,才能让该方法处理请求。
mybatis
mybatis 中 #{}和 ${}的区别是什么?
-
{}是预编译处理,${}是字符串替换;
-
Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
-
Mybatis在处理{}时,就是把{}替换成变量的值;
-
使用#{}可以有效的防止SQL注入,提高系统安全性。
mybatis 有几种分页方式?
1.数组分页
2.sql分页
3.拦截器分页
4.RowBounds分页
mybatis 逻辑分页和物理分页的区别是什么?
-
物理分页速度上并不一定快于逻辑分页,逻辑分页速度上也并不一定快于物理分页。
-
物理分页总是优于逻辑分页:没有必要将属于数据库端的压力加诸到应用端来,就算速度上存在优势,然而其它性能上的优点足以弥补这个缺点。
mybatis 是否支持延迟加载?延迟加载的原理是什么?
Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。
说一下 mybatis 的一级缓存和二级缓存?
一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置<cache/> ;
对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。
mybatis 和 hibernate 的区别有哪些?
(1)Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句。
(2)Mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套sql映射文件,工作量大。
(3)Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用hibernate开发可以节省很多代码,提高效率。
mybatis 有哪些执行器(Executor)?
Mybatis有三种基本的执行器(Executor):
SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。
BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。
mybatis 分页插件的实现原理是什么?
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
SpringBoot
什么是 spring boot?
在Spring框架这个大家族中,产生了很多衍生框架,比如 Spring、SpringMvc框架等,Spring的核心内容在于控制反转(IOC)和依赖注入(DI),所谓控制反转并非是一种技术,而是一种思想,在操作方面是指在spring配置文件中创建<bean>,依赖注入即为由spring容器为应用程序的某个对象提供资源,比如 引用对象、常量数据等。
SpringBoot是一个框架,一种全新的编程规范,他的产生简化了框架的使用,所谓简化是指简化了Spring众多框架中所需的大量且繁琐的配置文件,所以 SpringBoot是一个服务于框架的框架,服务范围是简化配置文件。
为什么要用 spring boot?
Spring Boot使编码变简单
Spring Boot使配置变简单
Spring Boot使部署变简单
Spring Boot使监控变简单
spring boot 核心配置文件是什么?
Spring Boot提供了两种常用的配置文件:
-
properties文件
-
yml文件
spring boot 配置文件有哪几种类型?它们有什么区别?
Spring Boot提供了两种常用的配置文件,分别是properties文件和yml文件。相对于properties文件而言,yml文件更年轻,也有很多的坑。可谓成也萧何败萧何,yml通过空格来确定层级关系,使配置文件结构跟清晰,但也会因为微不足道的空格而破坏了层级关系。
spring boot 有哪些方式可以实现热部署?
SpringBoot热部署实现有两种方式:
①. 使用spring loaded
添加完毕后需要使用mvn指令运行:
首先找到IDEA中的Edit configurations ,然后进行如下操作:(点击左上角的”+”,然后选择maven将出现右侧面板,在红色划线部位输入如图所示指令,你可以为该指令命名(此处命名为MvnSpringBootRun))
点击保存将会在IDEA项目运行部位出现,点击绿色箭头运行即可
②. 使用spring-boot-devtools
在项目的pom文件中添加依赖:
面向对象的编程思想
面向过程的编程思想就是在面对问题的时候使用计算机能够理解的逻辑来描述和表达待解决的问题以及具体的求解过程。例如写一个1+1等于2的程序,你会自然的想到输入两个变量,每个变量的值为1,然后对这两个变量求和,这就是面向过程的编程思想。
public static void main(String[] args) { int a=1; int b=1; int result=a+b; System.out.println(result); } 好的代码要达到可维护,可复用,可扩展以及灵活性好的四个特点。可维护就是需要修改的时候,只需要修改变动的那一部分,使得修改达到最小;可复用指代码不仅在写的地方使用,在别处依然可以调用这段代码;可扩展指当后来者想要扩展功能的时候不需要在之前代码修改的基础上进行修改。灵活性好,例如在代码实现功能方面,不仅实现了需求实现代码,还能够随时满足其它功能的扩展。
那么如何实现上述四个特点呢,面向对象编程则能很好的实现。为什么面向对象编程能够实现上述四个特点呢,因为面向对象编程本身具有的三个特点:封装,继承,多态。
封装:就是把对象的属性和行为(数据)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节,就是把不想告诉或者不该告诉别人的东西隐藏起来,把可以告诉别人的公开,别人只能用我提供的功能实现需求,而不知道是如何实现的。增加安全性
继承:是面向对象最显著的一个特性,继承是从已有的类中派生出新的类称为子类,子类继承父类的数据属性和行为,并能根据自己的需求扩展出新的行为,提高了代码的复用性。
多态:指允许不同的对象对同一消息做出相应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式(发送消息就是函数调用)。封装和继承几乎都是为多态而准备的,在执行期间判断引用对象的实际类型,根据其实际的类型调用其相应的方法。
如何使用上述思想编程呢,还是上面那个例子,1+1等于2还可以这么写:把加法当作一种对象,那么很自然的可以想到减法,除法,和乘法,他们都具有两个操作数以及进行操作的共同点,因此可以写一个共同的类,这个类里包含了这些操作的共同的部分,再写一个加法就继承这个共同的类,然后实现加法部分就可以完成我们需要的。
public abstract class Operation { public abstract int operation(int a,int b); }
public class Add extends Operation { @Override public int operation(int a, int b) { return add(a, b); }
private int add(int a, int b) { return a + b; }
}
public static void main(String[] args) { Operation add = new Add; System.out.println(add.operation(1, 1)); } 这篇文章主要展示了面向编程的思想方法,比较简单明了,如果想要了解更好的编程经验请看下一篇博客。
参考书籍:大话设计模式 ———————————————— 版权声明:本文为CSDN博主「jiaozi132」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/jiaozi132/article/details/82876805
Spring Cloud
-
spring cloud 的核心组件有哪些?
①. 服务发现——Netflix Eureka
一个RESTful服务,用来定位运行在AWS地区(Region)中的中间层服务。由两个组件组成:Eureka服务器和Eureka客户端。Eureka服务器用作服务注册服务器。Eureka客户端是一个java客户端,用来简化与服务器的交互、作为轮询负载均衡器,并提供服务的故障切换支持。Netflix在其生产环境中使用的是另外的客户端,它提供基于流量、资源利用率以及出错状态的加权负载均衡。
②. 客服端负载均衡——Netflix Ribbon
Ribbon,主要提供客户侧的软件负载均衡算法。Ribbon客户端组件提供一系列完善的配置选项,比如连接超时、重试、重试算法等。Ribbon内置可插拔、可定制的负载均衡组件。
③. 断路器——Netflix Hystrix
断路器可以防止一个应用程序多次试图执行一个操作,即很可能失败,允许它继续而不等待故障恢复或者浪费 CPU 周期,而它确定该故障是持久的。断路器模式也使应用程序能够检测故障是否已经解决。如果问题似乎已经得到纠正,应用程序可以尝试调用操作。
④. 服务网关——Netflix Zuul
类似nginx,反向代理的功能,不过netflix自己增加了一些配合其他组件的特性。
⑤. 分布式配置——Spring Cloud Config
这个还是静态的,得配合Spring Cloud Bus实现动态的配置更新。
集合
线程‘
mysql事务管理
数据库的三范式是什么?
-
第一范式:强调的是列的原子性,即数据库表的每一列都是不可分割的原子数据项。
-
第二范式:要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性。
-
第三范式:任何非主属性不依赖于其它非主属性。
一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 mysql 数据库,又插入了一条数据,此时 id 是几?
-
表类型如果是 MyISAM ,那 id 就是 18。
-
表类型如果是 InnoDB,那 id 就是 15。
InnoDB 表只会把自增主键的最大 id 记录在内存中,所以重启之后会导致最大 id 丢失。
如何获取当前数据库版本?
使用 select version() 获取当前 MySQL 数据库版本。
说一下 ACID 是什么?
-
Atomicity(原子性):一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
-
Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
-
Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
-
Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
char 和 varchar 的区别是什么?
char(n) :固定长度类型,比如订阅 char(10),当你输入”abc”三个字符的时候,它们占的空间还是 10 个字节,其他 7 个是空字节。
chat 优点:效率高;缺点:占用空间;适用场景:存储密码的 md5 值,固定长度的,使用 char 非常合适。
varchar(n) :可变长度,存储的值是每个值占用的字节再加上一个用来记录其长度的字节的长度。
所以,从空间上考虑 varcahr 比较合适;从效率上考虑 char 比较合适,二者使用需要权衡。
float 和 double 的区别是什么?
-
float 最多可以存储 8 位的十进制数,并在内存中占 4 字节。
-
double 最可可以存储 16 位的十进制数,并在内存中占 8 字节。
mysql 的内连接、左连接、右连接有什么区别?
内连接关键字:inner join;左连接:left join;右连接:right join。
内连接是把匹配的关联数据显示出来;左连接是左边的表全部显示出来,右边的表显示出符合条件的数据;右连接正好相反。
mysql 索引是怎么实现的?
索引是满足某种特定查找算法的数据结构,而这些数据结构会以某种方式指向数据,从而实现高效查找数据。
具体来说 MySQL 中的索引,不同的数据引擎实现有所不同,但目前主流的数据库引擎的索引都是 B+ 树实现的,B+ 树的搜索效率,可以到达二分法的性能,找到数据区域之后就找到了完整的数据结构了,所有索引的性能也是更好的。
怎么验证 mysql 的索引是否满足需求?
使用 explain 查看 SQL 是如何执行查询语句的,从而分析你的索引是否满足需求。
explain 语法:explain select * from table where type=1。
说一下数据库的事务隔离?
MySQL 的事务隔离是在 MySQL. ini 配置文件里添加的,在文件的最后添加:transaction-isolation = REPEATABLE-READ
可用的配置值:READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE。
-
READ-UNCOMMITTED:未提交读,最低隔离级别、事务未提交前,就可被其他事务读取(会出现幻读、脏读、不可重复读)。
-
READ-COMMITTED:提交读,一个事务提交后才能被其他事务读取到(会造成幻读、不可重复读)。
-
REPEATABLE-READ:可重复读,默认级别,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致,禁止读取到别的事务未提交的数据(会造成幻读)。
-
SERIALIZABLE:序列化,代价最高最可靠的隔离级别,该隔离级别能防止脏读、不可重复读、幻读。
脏读 :表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录 A,此时该事务还未提交,然后另一个事务尝试读取到了记录 A。
不可重复读 :是指在一个事务内,多次读同一数据。
幻读 :指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。
说一下 mysql 常用的引擎?
InnoDB 引擎:InnoDB 引擎提供了对数据库 acid 事务的支持,并且还提供了行级锁和外键的约束,它的设计的目标就是处理大数据容量的数据库系统。MySQL 运行的时候,InnoDB 会在内存中建立缓冲池,用于缓冲数据和索引。但是该引擎是不支持全文搜索,同时启动也比较的慢,它是不会保存表的行数的,所以当进行 select count(*) from table 指令的时候,需要进行扫描全表。由于锁的粒度小,写操作是不会锁定全表的,所以在并发度较高的场景下使用会提升效率的。
MyIASM 引擎:MySQL 的默认引擎,但不提供事务的支持,也不支持行级锁和外键。因此当执行插入和更新语句时,即执行写操作的时候需要锁定这个表,所以会导致效率会降低。不过和 InnoDB 不同的是,MyIASM 引擎是保存了表的行数,于是当进行 select count(*) from table 语句时,可以直接的读取已经保存的值而不需要进行扫描全表。所以,如果表的读操作远远多于写操作时,并且不需要事务的支持的,可以将 MyIASM 作为数据库引擎的首选。
说一下 mysql 的行锁和表锁?
MyISAM 只支持表锁,InnoDB 支持表锁和行锁,默认为行锁。
-
表级锁:开销小,加锁快,不会出现死锁。锁定粒度大,发生锁冲突的概率最高,并发量最低。
-
行级锁:开销大,加锁慢,会出现死锁。锁力度小,发生锁冲突的概率小,并发度最高。
乐观锁和悲观锁?
-
乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。
-
悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻止,直到这个锁被释放。
数据库的乐观锁需要自己实现,在表里面添加一个 version 字段,每次修改成功值加 1,这样每次修改的时候先对比一下,自己拥有的 version 和数据库现在的 version 是否一致,如果不一致就不修改,这样就实现了乐观锁。
mysql 问题排查都有哪些手段?
-
使用 show processlist 命令查看当前所有连接信息。
-
使用 explain 命令查询 SQL 语句执行计划。
-
开启慢查询日志,查看慢查询的 SQL。
如何做 mysql 的性能优化?
-
为搜索字段创建索引。
-
避免使用 select *,列出需要查询的字段。
-
垂直分割分表。
-
选择正确的存储引擎。
SQL语句
JVM
说一下 jvm 的主要组成部分?及其作用?
-
类加载器(ClassLoader)
-
运行时数据区(Runtime Data Area)
-
执行引擎(Execution Engine)
-
本地库接口(Native Interface)
组件的作用: 首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
说一下 jvm 运行时数据区?
-
程序计数器
-
虚拟机栈
-
本地方法栈
-
堆
-
方法区
有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户进程的启动和结束而创建和销毁。
说一下堆栈的区别?
\1. 栈内存存储的是局部变量而堆内存存储的是实体;
\2. 栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;
\3. 栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。
-
队列和栈是什么?有什么区别?
-
队列和栈都是被用来预存储数据的。
-
队列允许先进先出检索元素,但也有例外的情况,Deque 接口允许从两端检索元素。
-
栈和队列很相似,但它运行对元素进行后进先出进行检索。
什么是双亲委派模型?
在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。
类加载器分类:
-
启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
-
其他类加载器:
-
扩展类加载器(Extension ClassLoader):负责加载<java_home style="box-sizing: border-box; -webkit-tap-highlight-color: transparent; text-size-adjust: none; -webkit-font-smoothing: antialiased; outline: 0px !important;">\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库;</java_home>
-
应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
说一下类加载的执行过程?
类加载分为以下 5 个步骤:
加载:根据查找路径找到相应的 class 文件然后导入;
检查:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作。
怎么判断对象是否可以被回收?
一般有两种方法来判断:
-
引用计数器:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
-
可达性分析:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
java 中都有哪些引用类型?
-
强引用
-
软引用
-
弱引用
-
虚引用(幽灵引用/幻影引用)
说一下 jvm 有哪些垃圾回收算法?
-
标记-清除算法
-
标记-整理算法
-
复制算法
-
分代算法
说一下 jvm 有哪些垃圾回收器?
-
Serial:最早的单线程串行垃圾回收器。
-
Serial Old:Serial 垃圾回收器的老年版本,同样也是单线程的,可以作为 CMS 垃圾回收器的备选预案。
-
ParNew:是 Serial 的多线程版本。
-
Parallel 和 ParNew 收集器类似是多线程的,但 Parallel 是吞吐量优先的收集器,可以牺牲等待时间换取系统的吞吐量。
-
Parallel Old 是 Parallel 老生代版本,Parallel 使用的是复制的内存回收算法,Parallel Old 使用的是标记-整理的内存回收算法。
-
CMS:一种以获得最短停顿时间为目标的收集器,非常适用 B/S 系统。
-
G1:一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项。
详细介绍一下 CMS 垃圾回收器?
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?
-
新生代回收器:Serial、ParNew、Parallel Scavenge
-
老年代回收器:Serial Old、Parallel Old、CMS
-
整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
简述分代垃圾回收器是怎么工作的?
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
-
把 Eden + From Survivor 存活的对象放入 To Survivor 区;
-
清空 Eden 和 From Survivor 分区;
-
From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
说一下 jvm 调优的工具?
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
-
jconsole:用于对 JVM 中的内存、线程和类等进行监控;
-
jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
常用的 jvm 调优的参数都有哪些?
-
-Xms2g:初始化推大小为 2g;
-
-Xmx2g:堆最大内存为 2g;
-
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
-
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-
-XX:+PrintGC:开启打印 gc 信息;
-
-XX:+PrintGCDetails:打印 gc 详细信息。
数据结构
1) 红黑树的了解(平衡树,二叉搜索树),使用场景 把数据结构上几种树集中的讨论一下:
1.AVLtree
定义:最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是O(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。
节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树。平衡因子可以直接存储在每个节点中,或从可能存储在节点中的子树高度计算出来。 一般我们所看见的都是排序平衡二叉树。
AVLtree使用场景:AVL树适合用于插入删除次数比较少,但查找多的情况。插入删除导致很多的旋转,旋转是非常耗时的。AVL 在linux内核的vm area中使用。
2.二叉搜索树
二叉搜索树也是一种树,适用与一般二叉树的全部操作,但二叉搜索树能够实现数据的快速查找。
二叉搜索树满足的条件:
1.非空左子树的所有键值小于其根节点的键值 2.非空右子树的所有键值大于其根节点的键值 3.左右子树都是二叉搜索树
二叉搜索树的应用场景:如果是没有退化称为链表的二叉树,查找效率就是lgn,效率不错,但是一旦退换称为链表了,要么使用平衡二叉树,或者之后的RB树,因为链表就是线性的查找效率。
3.红黑树的定义
红黑树是一种二叉查找树,但在每个结点上增加了一个存储位表示结点的颜色,可以是RED或者BLACK。通过对任何一条从根到叶子的路径上各个着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。
当二叉查找树的高度较低时,这些操作执行的比较快,但是当树的高度较高时,这些操作的性能可能不比用链表好。红黑树(red-black tree)是一种平衡的二叉查找树,它能保证在最坏情况下,基本的动态操作集合运行时间为O(lgn)。
红黑树必须要满足的五条性质:
性质一:节点是红色或者是黑色; 在树里面的节点不是红色的就是黑色的,没有其他颜色,要不怎么叫红黑树呢,是吧。
性质二:根节点是黑色; 根节点总是黑色的。它不能为红。
性质三:每个叶节点(NIL或空节点)是黑色;
性质四:每个红色节点的两个子节点都是黑色的(也就是说不存在两个连续的红色节点); 就是连续的两个节点不能是连续的红色,连续的两个节点的意思就是父节点与子节点不能是连续的红色。
性质五:从任一节点到其每个叶节点的所有路径都包含相同数目的黑色节点。从根节点到每一个NIL节点的路径中,都包含了相同数量的黑色节点。
红黑树的应用场景:红黑树是一种不是非常严格的平衡二叉树,没有AVLtree那么严格的平衡要求,所以它的平均查找,增添删除效率都还不错。广泛用在C++的STL中。如map和set都是用红黑树实现的。
4.B树定义
B树和平衡二叉树稍有不同的是B树属于多叉树又名平衡多路查找树(查找路径不只两个),不属于二叉搜索树的范畴,因为它不止两路,存在多路。
B树满足的条件:
(1)树种的每个节点最多拥有m个子节点且m>=2,空树除外(注:m阶代表一个树节点最多有多少个查找路径,m阶=m路,当m=2则是2叉树,m=3则是3叉); (2)除根节点外每个节点的关键字数量大于等于ceil(m/2)-1个小于等于m-1个,非根节点关键字数必须>=2;(注:ceil()是个朝正无穷方向取整的函数 如ceil(1.1)结果为2) (3)所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针只不过其指针地址都为null对应下图最后一层节点的空格子 (4)如果一个非叶节点有N个子节点,则该节点的关键字数等于N-1; (5)所有节点关键字是按递增次序排列,并遵循左小右大原则;
B树的应用场景:构造一个多阶的B类树,然后在尽量多的在结点上存储相关的信息,保证层数尽量的少,以便后面我们可以更快的找到信息,磁盘的I/O操作也少一些,而且B类树是平衡树,每个结点到叶子结点的高度都是相同,这也保证了每个查询是稳定的。
5.B+树
B+树是B树的一个升级版,B+树是B树的变种树,有n棵子树的节点中含有n个关键字,每个关键字不保存数据,只用来索引,数据都保存在叶子节点。是为文件系统而生的。
相对于B树来说B+树更充分的利用了节点的空间,让查询速度更加稳定,其速度完全接近于二分法查找。为什么说B+树查找的效率要比B树更高、更稳定;我们先看看两者的区别
(1)B+跟B树不同,B+树的非叶子节点不保存关键字记录的指针,这样使得B+树每个节点所能保存的关键字大大增加; (2)B+树叶子节点保存了父节点的所有关键字和关键字记录的指针,每个叶子节点的关键字从小到大链接; (3)B+树的根节点关键字数量和其子节点个数相等; (4)B+的非叶子节点只进行数据索引,不会存实际的关键字记录的指针,所有数据地址必须要到叶子节点才能获取到,所以每次数据查询的次数都一样;
特点: 在B树的基础上每个节点存储的关键字数更多,树的层级更少所以查询数据更快,所有指关键字指针都存在叶子节点,所以每次查找的次数都相同所以查询速度更稳定;
应用场景: 用在磁盘文件组织 数据索引和数据库索引。
6.Trie树(字典树)
trie,又称前缀树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
在图示中,键标注在节点中,值标注在节点之下。每一个完整的英文单词对应一个特定的整数。Trie 可以看作是一个确定有限状态自动机,尽管边上的符号一般是隐含在分支的顺序中的。 键不需要被显式地保存在节点中。图示中标注出完整的单词,只是为了演示 trie 的原理。
trie树的优点:利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。缺点:Trie树是一种比较简单的数据结构.理解起来比较简单,正所谓简单的东西也得付出代价.故Trie树也有它的缺点,Trie树的内存消耗非常大.
其基本性质可以归纳为:
根节点不包含字符,除根节点外每一个节点都只包含一个字符。 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。 每个节点的所有子节点包含的字符都不相同。 典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。字典树与字典很相似,当你要查一个单词是不是在字典树中,首先看单词的第一个字母是不是在字典的第一层,如果不在,说明字典树里没有该单词,如果在就在该字母的孩子节点里找是不是有单词的第二个字母,没有说明没有该单词,有的话用同样的方法继续查找.字典树不仅可以用来储存字母,也可以储存数字等其它数据。
(2) 红黑树在STL上的应用 STL中set、multiset、map、multimap底层是红黑树实现的,而unordered_map、unordered_set 底层是哈希表实现的。
multiset、multimap: 插入相同的key的时候,我们将后插入的key放在相等的key的右边,之后不管怎么进行插入或删除操作,后加入的key始终被认为比之前的大。
(3) 了解并查集吗?(低频) 什么是合并查找问题呢?
顾名思义,就是既有合并又有查找操作的问题。举个例子,有一群人,他们之间有若干好友关系。如果两个人有直接或者间接好友关系,那么我们就说他们在同一个朋友圈中,这里解释下,如果Alice是Bob好友的好友,或者好友的好友的好友等等,即通过若干好友可以认识,那么我们说Alice和Bob是间接好友。随着时间的变化,这群人中有可能会有新的朋友关系,这时候我们会对当中某些人是否在同一朋友圈进行询问。这就是一个典型的合并-查找操作问题,既包含了合并操作,又包含了查找操作。
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。
并查集也是使用树形结构实现。不过,不是二叉树。每个元素对应一个节点,每个组对应一棵树。在并查集中,哪个节点是哪个节点的父亲以及树的形状等信息无需多加关注,整体组成一个树形结构才是重要的。类似森林
(4) 贪心算法和动态规划的区别 贪心算法:局部最优,划分的每个子问题都最优,得到全局最优,但是不能保证是全局最优解,所以对于贪心算法来说,解是从上到下的,一步一步最优,直到最后。
动态规划:将问题分解成重复的子问题,每次都寻找左右子问题解中最优的解,一步步得到全局的最优解.重复的子问题可以通过记录的方式,避免多次计算。所以对于动态规划来说,解是从小到上,从底层所有可能性中找到最优解,再一步步向上。
分治法:和动态规划类似,将大问题分解成小问题,但是这些小问题是独立的,没有重复的问题。独立问题取得解,再合并成大问题的解。
例子:比如钱币分为1元3元4元,要拿6元钱,贪心的话,先拿4,再拿两个1,一共3张钱;实际最优却是两张3元就够了。
(5) 判断一个链表是否有环,如何找到这个环的起点 给定一个单链表,只给出头指针h: 1、如何判断是否存在环? 2、如何知道环的长度? 3、如何找出环的连接点在哪里? 4、带环链表的长度是多少?
解法: 1、对于问题1,使用追赶的方法,设定两个指针slow、fast,从头指针开始,每次分别前进1步、2步。如存在环,则两者相遇;如不存在环,fast遇到NULL退出。 2、对于问题2,记录下问题1的碰撞点p,slow、fast从该点开始,再次碰撞所走过的操作数就是环的长度s。 3、问题3:有定理:碰撞点p到连接点的距离=头指针到连接点的距离,因此,分别从碰撞点、头指针开始走,相遇的那个点就是连接点。(证明在后面附注) 4、问题3中已经求出连接点距离头指针的长度,加上问题2中求出的环的长度,二者之和就是带环单链表的长度 http://blog.sina.com.cn/s/blog_725dd1010100tqwp.html
(6) 实现一个strcpy函数(或者memcpy),如果内存可能重叠呢 ——大家一般认为名不见经传strcpy函数实现不是很难,流行的strcpy函数写法是:
-
char *my_strcpy(char *dst,const char *src)
-
{
-
assert(dst != NULL);
-
assert(src != NULL);
-
char *ret = dst;
-
while((* dst++ = * src++) != '\0')
-
;
-
return ret;
-
} 1 2 3 4 5 6 7 8 9 如果注意到: 1,检查指针有效性; 2,返回目的指针des; 3,源字符串的末尾 ‘\0’ 需要拷贝。
内存重叠 内存重叠:拷贝的目的地址在源地址范围内。所谓内存重叠就是拷贝的目的地址和源地址有重叠。 在函数strcpy和函数memcpy都没有对内存重叠做处理的,使用这两个函数的时候只有程序员自己保证源地址和目标地址不重叠,或者使用memmove函数进行内存拷贝。 memmove函数对内存重叠做了处理。 strcpy的正确实现应为:
-
char *my_strcpy(char *dst,const char *src)
-
{
-
assert(dst != NULL);
-
assert(src != NULL);
-
char *ret = dst;
-
memmove(dst,src,strlen(src)+1);
-
return ret;
-
} 1 2 3 4 5 6 7 8 memmove函数实现时考虑到了内存重叠的情况,可以完成指定大小的内存拷贝
(7) 快排存在的问题,如何优化
快排的时间复杂度 时间复杂度最快平均是O(nlogn),最慢的时候是O(n2);辅助空间也是O(logn);最开始学快排时最疑惑的就是这个东西不知道怎么得来的,一种是通过数学运算可以的出来,还有一种是通过递归树来理解就容易多了
这张图片别人博客那里弄过来的,所谓时间复杂度最理想的就是取到中位数情况,那么递归树就是一个完全二叉树,那么树的深度也就是最低为Logn,这个时候每一次又需要n次比较,所以时间复杂度nlogn,当快排为顺序或者逆序时,这个数为一个斜二叉树,深度为n,同样每次需要n次比较,那那么最坏需要n2的时间
优化: 1.当整个序列有序时退出算法; 2.当序列长度很小时(根据经验是大概小于 8),应该使用常数更小的算法,比如插入排序等; 3.随机选取分割位置; 4.当分割位置不理想时,考虑是否重新选取分割位置; 5.分割成两个序列时,只对其中一个递归进去,另一个序列仍可以在这一函数内继续划分,可以显著减小栈的大小(尾递归): 6.将单向扫描改成双向扫描,可以减少划分过程中的交换次数
优化1:当待排序序列的长度分割到一定大小后,使用插入排序 原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排
优化2:在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割
优化3:优化递归操作 快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化
优点:如果待排序的序列划分极端不平衡,递归的深度将趋近于n,而栈的大小是很有限的,每次递归调用都会耗费一定的栈空间,函数的参数越多,每次递归耗费的空间也越多。优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(logn),将会提高性能。
(8) Top K问题(可以采取的方法有哪些,各自优点?)
1.将输入内容(假设用数组存放)进行完全排序,从中选出排在前K的元素即为所求。有了这个思路,我们可以选择相应的排序算法进行处理,目前来看快速排序,堆排序和归并排序都能达到O(nlogn)的时间复杂度。
2.对输入内容进行部分排序,即只对前K大的元素进行排序(这K个元素即为所求)。此时我们可以选择冒泡排序或选择排序进行处理,即每次冒泡(选择)都能找到所求的一个元素。这类策略的时间复杂度是O(Kn)。
3.对输入内容不进行排序,显而易见,这种策略将会有更好的性能开销。我们此时可以选择两种策略进行处理:
用一个桶来装前k个数,桶里面可以按照最小堆来维护 a)利用最小堆维护一个大小为K的数组,目前该小根堆中的元素是排名前K的数,其中根是最小的数。此后,每次从原数组中取一个元素与根进行比较,如大于根的元素,则将根元素替换并进行堆调整(下沉),即保证小根堆中的元素仍然是排名前K的数,且根元素仍然最小;否则不予处理,取下一个数组元素继续该过程。该算法的时间复杂度是O(nlogK),一般来说企业中都采用该策略处理top-K问题,因为该算法不需要一次将原数组中的内容全部加载到内存中,而这正是海量数据处理必然会面临的一个关卡。
b)利用快速排序的分划函数找到分划位置K,则其前面的内容即为所求。该算法是一种非常有效的处理方式,时间复杂度是O(n)(证明可以参考算法导论书籍)。对于能一次加载到内存中的数组,该策略非常优秀。
(9) Bitmap的使用,存储和插入方法
BitMap从字面的意思 很多人认为是位图,其实准确的来说,翻译成基于位的映射。 在所有具有性能优化的数据结构中,大家使用最多的就是hash表,是的,在具有定位查找上具有O(1)的常量时间,多么的简洁优美。但是数据量大了,内存就不够了。 当然也可以使用类似外排序来解决问题的,由于要走IO所以时间上又不行。 所谓的Bit-map就是用一个bit位来标记某个元素对应的Value, 而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。 其实如果你知道计数排序的话(算法导论中有一节讲过),你就会发现这个和计数排序很像。
bitmap应用 1)可进行数据的快速查找,判重,删除,一般来说数据范围是int的10倍以下。 2)去重数据而达到压缩数据 1 2 还可以用于爬虫系统中url去重、解决全组合问题。
BitMap应用:排序示例
假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复)。那么我们就可以采用Bit-map的方法来达到排序的目的。要表示8个数,我们就只需要8个Bit(1Bytes),首先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0(如下图:)
然后遍历这5个元素,首先第一个元素是4,那么就把4对应的位置为1(可以这样操作 p+(i/8)|(0×01<<(i%8)) 当然了这里的操作涉及到Big-ending和Little-ending的情况,这里默认为Big-ending。不过计算机一般是小端存储的,如intel。小端的话就是将倒数第5位置1),因为是从零开始的,所以要把第五位置为一(如下图):
然后再处理第二个元素7,将第八位置为1,,接着再处理第三个元素,一直到最后处理完所有的元素,将相应的位置为1,这时候的内存的Bit位的状态如下:
然后我们现在遍历一遍Bit区域,将该位是一的位的编号输出(2,3,4,5,7),这样就达到了排序的目的。
bitmap排序复杂度分析 Bitmap排序需要的时间复杂度和空间复杂度依赖于数据中最大的数字。 bitmap排序的时间复杂度不是O(N)的,而是取决于待排序数组中的最大值MAX,在实际应用上关系也不大,比如我开10个线程去读byte数组,那么复杂度为:O(Max/10)。也就是要是读取的,可以用多线程的方式去读取。时间复杂度方面也是O(Max/n),其中Max为byte[]数组的大小,n为线程大小。 空间复杂度应该就是O(Max/8)bytes吧
BitMap算法流程 假设需要排序或者查找的最大数MAX=10000000(lz:这里MAX应该是最大的数而不是int数据的总数!),那么我们需要申请内存空间的大小为int a[1 + MAX/32]。 其中:a[0]在内存中占32为可以对应十进制数0-31,依次类推: bitmap表为: a[0]--------->0-31 a[1]--------->32-63 a[2]--------->64-95 a[3]--------->96-127 …
我们要把一个整数N映射到Bit-Map中去,首先要确定把这个N Mapping到哪一个数组元素中去,即确定映射元素的index。我们用int类型的数组作为map的元素,这样我们就知道了一个元素能够表示的数字个数(这里是32)。于是N/32就可以知道我们需要映射的key了。所以余下来的那个N%32就是要映射到的位数。
1.求十进制数对应在数组a中的下标: 先由十进制数n转换为与32的余可转化为对应在数组a中的下标。 如十进制数0-31,都应该对应在a[0]中,比如n=24,那么 n/32=0,则24对应在数组a中的下标为0。又比如n=60,那么n/32=1,则60对应在数组a中的下标为1,同理可以计算0-N在数组a中的下标。 i = N>>K % 结果就是N/(2^K) Note: map的范围是[0, 原数组最大的数对应的2的整次方数-1]。
2.求十进制数对应数组元素a[i]在0-31中的位m: 十进制数0-31就对应0-31,而32-63则对应也是0-31,即给定一个数n可以通过模32求得对应0-31中的数。 m = n & ((1 << K) - 1) %结果就是n%(2^K)
3.利用移位0-31使得对应第m个bit位为1 如a[i]的第m位置1:a[i] = a[i] | (1<<m) 如:将当前4对应的bit位置1的话,只需要1左移4位与B[0] | 即可。
Note:
1 p+(i/8)|(0×01<<(i%8))这样也可以? 2 同理将int型变量a的第k位清0,即a=a&~(1<<k)
BitMap算法评价 优点:
-
运算效率高,不进行比较和移位;
-
占用内存少,比如最大的数MAX=10000000;只需占用内存为MAX/8=1250000Byte=1.25M。 3. 缺点:
-
所有的数据不能重复,即不可对重复的数据进行排序。(少量重复数据查找还是可以的,用2-bitmap)。
-
当数据类似(1,1000,10万)只有3个数据的时候,用bitmap时间复杂度和空间复杂度相当大,只有当数据比较密集时才有优势。 http://blog.csdn.net/pipisorry/article/details/62443757
(10) 字典树的理解以及在统计上的应用
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。Trie树也有它的缺点,Trie树的内存消耗非常大.当然,或许用左儿子右兄弟的方法建树的话,可能会好点.
就是在海量数据中找出某一个数,比如2亿QQ号中查找出某一个特定的QQ号。。
(11) N个骰子出现和为m的概率
典型的可以用动态规划的思想来完成
1.现在变量有:骰子个数,点数和。当有k个骰子,点数和为n时,出现次数记为f(k,n)。那与k-1个骰子阶段之间的关系是怎样的?
2.当我有k-1个骰子时,再增加一个骰子,这个骰子的点数只可能为1、2、3、4、5或6。那k个骰子得到点数和为n的情况有: (k-1,n-1):第k个骰子投了点数1 (k-1,n-2):第k个骰子投了点数2 (k-1,n-3):第k个骰子投了点数3 … (k-1,n-6):第k个骰子投了点数6 在k-1个骰子的基础上,再增加一个骰子出现点数和为n的结果只有这6种情况! 所以:f(k,n)=f(k-1,n-1)+f(k-1,n-2)+f(k-1,n-3)+f(k-1,n-4)+f(k-1,n-5)+f(k-1,n-6)
3.有1个骰子,f(1,1)=f(1,2)=f(1,3)=f(1,4)=f(1,5)=f(1,6)=1。 用递归就可以解决这个问题:
用迭代来完成
(19) 海量数据问题(可参考左神的书)
目前关于海量数据想到的解决办法: 1.bitmap 2.桶排序,外部排序,将需要排序的放到外存上,不用全部放到内存上
(20) 一致性哈希
说明 http://www.zsythink.net/archives/1182
优点 1.当后端是缓存服务器时,经常使用一致性哈希算法来进行负载均衡。使用一致性哈希的好处在于,增减集群的缓存服务器时,只有少量的缓存会失效,回源量较小。 2.尽量减少数据丢失问题,减少移动数据的风险 ———————————————— 版权声明:本文为CSDN博主「祚儿疯」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/u012414189/article/details/83832161
设计模式
3.1. 单例模式(singleton)
有些时候,允许自由创建某个类的实例没有意义,还可能造成系统性能下降。如果一个类始终只能创建一个实例,则这个类被称为单例类,这种模式就被称为单例模式。
一般建议单例模式的方法命名为:getInstance(),这个方法的返回类型肯定是单例类的类型了。getInstance方法可以有参数,这些参数可能是创建类实例所需要的参数,当然,大多数情况下是不需要的
*public**class* Singleton { *public**static**void* main(String[] args) { //创建Singleton对象不能通过构造器,只能通过getInstance方法 Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); //将输出true System.out.println(s1 == s2); } //使用一个变量来缓存曾经创建的实例 *private**static* Singleton instance; //将构造器使用private修饰,隐藏该构造器 *private* Singleton(){ System.out.println("Singleton被构造!"); } //提供一个静态方法,用于返回Singleton实例 //该方法可以加入自定义的控制,保证只产生一个Singleton对象 *public**static* Singleton getInstance() { //如果instance为null,表明还不曾创建Singleton对象 //如果instance不为null,则表明已经创建了Singleton对象,将不会执行该方法 *if* (instance == *null*) { //创建一个Singleton对象,并将其缓存起来 instance = *new* Singleton(); } *return*instance; }}
单例模式主要有如下两个优势:
1) 减少创建Java实例所带来的系统开销
2) 便于系统跟踪单个Java实例的生命周期、实例状态等。
3.2. 简单工厂(StaticFactory Method)
简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式,可以理解为是不同工厂模式的一个特殊实现。
A实例调用B实例的方法,称为A依赖于B。如果使用new关键字来创建一个B实例(硬编码耦合),然后调用B实例的方法。一旦系统需要重构:需要使用C类来代替B类时,程序不得不改写A类代码。而用工厂模式则不需要关心B对象的实现、创建过程。
Output,接口
*public**interface* Output{ //接口里定义的属性只能是常量 *int*MAX_CACHE_LINE = 50; //接口里定义的只能是public的抽象实例方法 *void* out(); *void* getData(String msg);}
Printer,Output的一个实现
//让Printer类实现Output*public**class* Printer *implements* Output{ *private* String[] printData = *new* String[MAX_CACHE_LINE]; //用以记录当前需打印的作业数 *private**int*dataNum = 0; *public**void* out() { //只要还有作业,继续打印 *while*(dataNum > 0) { System.out.println("打印机打印:" + printData[0]); //把作业队列整体前移一位,并将剩下的作业数减1 System.arraycopy(printData , 1, printData, 0, --dataNum); } } *public**void* getData(String msg) { *if* (dataNum >= MAX_CACHE_LINE) { System.out.println("输出队列已满,添加失败"); } *else* { //把打印数据添加到队列里,已保存数据的数量加1。 printData[dataNum++] = msg; } }}
BetterPrinter,Output的一个实现
*public**class* BetterPrinter *implements* Output{ *private* String[] printData = *new* String[MAX_CACHE_LINE * 2]; //用以记录当前需打印的作业数 *private**int*dataNum = 0; *public**void* out() { //只要还有作业,继续打印 *while*(dataNum > 0) { System.out.println("高速打印机正在打印:" + printData[0]); //把作业队列整体前移一位,并将剩下的作业数减1 System.arraycopy(printData , 1, printData, 0, --dataNum); } } *public**void* getData(String msg) { *if* (dataNum >= MAX_CACHE_LINE * 2) { System.out.println("输出队列已满,添加失败"); } *else* { //把打印数据添加到队列里,已保存数据的数量加1。 printData[dataNum++] = msg; } }}
OutputFactory,简单工厂类
*public* Output getPrinterOutput(String type) { *if* (type.equalsIgnoreCase("better")) { *return**new* BetterPrinter(); } *else* { *return**new* Printer(); } }
Computer
*public**class* Computer{ *private* Output out; *public* Computer(Output out) { *this*.out = out; } //定义一个模拟获取字符串输入的方法 *public**void* keyIn(String msg) { out.getData(msg); } //定义一个模拟打印的方法 *public**void* print() { out.out(); } *public**static**void* main(String[] args) { //创建OutputFactory OutputFactory of = *new* OutputFactory(); //将Output对象传入,创建Computer对象 Computer c = *new* Computer(of.getPrinterOutput("normal")); c.keyIn("建筑永恒之道"); c.keyIn("建筑模式语言"); c.print(); c = *new* Computer(of.getPrinterOutput("better")); c.keyIn("建筑永恒之道"); c.keyIn("建筑模式语言"); c.print(); }
使用简单工厂模式的优势:让对象的调用者和对象创建过程分离,当对象调用者需要对象时,直接向工厂请求即可。从而避免了对象的调用者与对象的实现类以硬编码方式耦合,以提高系统的可维护性、可扩展性。工厂模式也有一个小小的缺陷:当产品修改时,工厂类也要做相应的修改。
3.3. 工厂方法(Factory Method)和抽象工厂(Abstract Factory)
如果我们不想在工厂类中进行逻辑判断,程序可以为不同产品类提供不同的工厂,不同的工厂类和产不同的产品。
当使用工厂方法设计模式时,对象调用者需要与具体的工厂类耦合,如:
//工厂类的定义1*public**class* BetterPrinterFactory *implements* OutputFactory{ *public* Output getOutput() { //该工厂只负责产生BetterPrinter对象 *return**new* BetterPrinter(); }}//工厂类的定义2*public**class* PrinterFactory *implements* OutputFactory{ *public* Output getOutput() { //该工厂只负责产生Printer对象 *return**new* Printer(); }}//工厂类的调用//OutputFactory of = new BetterPrinterFactory();OutputFactory of = *new* PrinterFactory();Computer c = *new* Computer(of.getOutput());
使用简单工厂类,需要在工厂类里做逻辑判断。而工厂类虽然不用在工厂类做判断。但是带来了另一种耦合:客户端代码与不同的工厂类耦合。
为了解决客户端代码与不同工厂类耦合的问题。在工厂类的基础上再增加一个工厂类,该工厂类不制造具体的被调用对象,而是制造不同工厂对象。如:
//抽象工厂类的定义,在工厂类的基础上再建一个工厂类*public**class* OutputFactoryFactory{ //仅定义一个方法用于返回输出设备。 *public**static* OutputFactory getOutputFactory( String type) { *if* (type.equalsIgnoreCase("better")) { *return**new* BetterPrinterFactory(); } *else* { *return**new* PrinterFactory(); } }} //抽象工厂类的调用OutputFactory of = OutputFactoryFactory.getOutputFactory("better");Computer c = *new* Computer(of.getOutput());
3.4. 代理模式(Proxy)
代理模式是一种应用非常广泛的设计模式,当客户端代码需要调用某个对象时,客户端实际上不关心是否准确得到该对象,它只要一个能提供该功能的对象即可,此时我们就可返回该对象的代理(Proxy)。
代理就是一个Java对象代表另一个Java对象来采取行动。如:
*public**class* ImageProxy *implements* Image{ //组合一个image实例,作为被代理的对象 *private* Image image; //使用抽象实体来初始化代理对象 *public* ImageProxy(Image image) { *this*.image = image; } /** * 重写Image接口的show()方法 * 该方法用于控制对被代理对象的访问, * 并根据需要负责创建和删除被代理对象 / ****public**void** show() { //只有当真正需要调用image的show方法时才创建被代理对象 **if* (image == *null*) { image = *new* BigImage(); } image.show(); }}
调用时,先不创建:
Image image = new ImageProxy(null);
Hibernate默认启用延迟加载,当系统加载A实体时,A实体关联的B实体并未被加载出来,A实体所关联的B实体全部是代理对象——只有等到A实体真正需要访问B实体时,系统才会去数据库里抓取B实体所对应的记录。
借助于Java提供的Proxy和InvocationHandler,可以实现在运行时生成动态代理的功能,而动态代理对象就可以作为目标对象使用,而且增强了目标对象的功能。如:
Panther
*public**interface* Panther{ //info方法声明 *public**void* info(); //run方法声明 *public**void* run();}
GunPanther
*public**class* GunPanther *implements* Panther{ //info方法实现,仅仅打印一个字符串 *public**void* info() { System.out.println("我是一只猎豹!"); } //run方法实现,仅仅打印一个字符串 *public**void* run() { System.out.println("我奔跑迅速"); }}
MyProxyFactory,创建代理对象
*public**class* MyProxyFactory{ //为指定target生成动态代理对象 *public**static* Object getProxy(Object target) *throws* Exception { //创建一个MyInvokationHandler对象 MyInvokationHandler handler = *new* MyInvokationHandler(); //为MyInvokationHandler设置target对象 handler.setTarget(target); //创建、并返回一个动态代理 *return* Proxy.newProxyInstance(target.getClass().getClassLoader() , target.getClass().getInterfaces(), handler); }}
MyInvokationHandler,增强代理的功能
*public**class* MyInvokationHandler *implements* InvocationHandler{ //需要被代理的对象 *private* Object target; *public**void* setTarget(Object target) { *this*.target = target; } //执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法 *public* Object invoke(Object proxy, Method method, Object[] args) *throws* Exception { TxUtil tx = *new* TxUtil(); //执行TxUtil对象中的beginTx。 tx.beginTx(); //以target作为主调来执行method方法 Object result = method.invoke(target , args); //执行TxUtil对象中的endTx。 tx.endTx(); *return* result; }}
TxUtil
*public**class* TxUtil{ //第一个拦截器方法:模拟事务开始 *public**void* beginTx() { System.out.println("=====模拟开始事务====="); } //第二个拦截器方法:模拟事务结束 *public**void* endTx() { System.out.println("=====模拟结束事务====="); }}
测试
*public**static**void* main(String[] args) *throws* Exception { //创建一个原始的GunDog对象,作为target Panther target = *new* GunPanther(); //以指定的target来创建动态代理 Panther panther = (Panther)MyProxyFactory.getProxy(target); //调用代理对象的info()和run()方法 panther.info(); panther.run(); }
Spring所创建的AOP代理就是这种动态代理。但是Spring AOP更灵活。
3.5. 命令模式(Command)
某个方法需要完成某一个功能,完成这个功能的大部分步骤已经确定了,但可能有少量具体步骤无法确定,必须等到执行该方法时才可以确定。(在某些编程语言如Ruby、Perl里,允许传入一个代码块作为参数。但Jara暂时还不支持代码块作为参数)。在Java中,传入该方法的是一个对象,该对象通常是某个接口的匿名实现类的实例,该接口通常被称为命令接口,这种设计方式也被称为命令模式。
如:
Command
*public**interface* Command{ //接口里定义的process方法用于封装“处理行为” *void* process(*int*[] target);}
ProcessArray
*public**class* ProcessArray{ //定义一个each()方法,用于处理数组, *public**void* each(*int*[] target , Command cmd) { cmd.process(target); }}
TestCommand
*public**class* TestCommand{ *public**static**void* main(String[] args) { ProcessArray pa = *new* ProcessArray(); *int*[] target = {3, -4, 6, 4}; //第一次处理数组,具体处理行为取决于Command对象 pa.each(target , *new* Command() { //重写process()方法,决定具体的处理行为 *public**void* process(*int*[] target) { *for* (*int* tmp : target ) { System.out.println("迭代输出目标数组的元素:" + tmp); } } }); System.out.println("------------------"); //第二次处理数组,具体处理行为取决于Command对象 pa.each(target , *new* Command() { //重写process方法,决定具体的处理行为 *public**void* process(*int*[] target) { *int* sum = 0; *for* (*int* tmp : target ) { sum += tmp; } System.out.println("数组元素的总和是:" + sum); } }); }}
HibernateTemplate使用了executeXxx()方法弥补了HibernateTemplate的不足,该方法需要接受一个HibernateCallback接口,该接口的代码如下:
public interface HibernateCallback{ Object doInHibernate(Session session);}
3.6. 策略模式(Strategy)
策略模式用于封装系列的算法,这些算法通常被封装在一个被称为Context的类中,客户端程序可以自由选择其中一种算法,或让Context为客户端选择一种最佳算法——使用策略模式的优势是为了支持算法的自由切换。
DiscountStrategy,折扣方法接口
*public**interface* DiscountStrategy{ //定义一个用于计算打折价的方法 *double* getDiscount(*double* originPrice);}
OldDiscount,旧书打折算法
*public**class* OldDiscount *implements* DiscountStrategy { // 重写getDiscount()方法,提供旧书打折算法 *public**double* getDiscount(*double* originPrice) { System.out.println("使用旧书折扣..."); *return* originPrice * 0.7; }}
VipDiscount,VIP打折算法
//实现DiscountStrategy接口,实现对VIP打折的算法*public**class* VipDiscount *implements* DiscountStrategy { // 重写getDiscount()方法,提供VIP打折算法 *public**double* getDiscount(*double* originPrice) { System.out.println("使用VIP折扣..."); *return* originPrice * 0.5; }}
策略定义
*public**class* DiscountContext{ //组合一个DiscountStrategy对象 *private* DiscountStrategy strategy; //构造器,传入一个DiscountStrategy对象 *public* DiscountContext(DiscountStrategy strategy) { *this*.strategy = strategy; } //根据实际所使用的DiscountStrategy对象得到折扣价 *public**double* getDiscountPrice(*double* price) { //如果strategy为null,系统自动选择OldDiscount类 *if* (strategy == *null*) { strategy = *new* OldDiscount(); } *return**this*.strategy.getDiscount(price); } //提供切换算法的方法 *public**void* setDiscount(DiscountStrategy strategy) { *this*.strategy = strategy; }}
测试
*public**static**void* main(String[] args) { //客户端没有选择打折策略类 DiscountContext dc = *new* DiscountContext(*null*); *double* price1 = 79; //使用默认的打折策略 System.out.println("79元的书默认打折后的价格是:" + dc.getDiscountPrice(price1)); //客户端选择合适的VIP打折策略 dc.setDiscount(*new* VipDiscount()); *double* price2 = 89; //使用VIP打折得到打折价格 System.out.println("89元的书对VIP用户的价格是:" + dc.getDiscountPrice(price2)); }
使用策略模式可以让客户端代码在不同的打折策略之间切换,但也有一个小小的遗憾:客户端代码需要和不同的策略耦合。为了弥补这个不足,我们可以考虑使用配置文件来指定DiscountContext使用哪种打折策略——这就彻底分离客户端代码和具体打折策略类。
3.7. 门面模式(Facade)
随着系统的不断改进和开发,它们会变得越来越复杂,系统会生成大量的类,这使得程序流程更难被理解。门面模式可为这些类提供一个简化的接口,从而简化访问这些类的复杂性。
门面模式(Facade)也被称为正面模式、外观模式,这种模式用于将一组复杂的类包装到一个简单的外部接口中。
原来的方式
// 依次创建三个部门实例 Payment pay = *new* PaymentImpl(); Cook cook = *new* CookImpl(); Waiter waiter = *new* WaiterImpl(); // 依次调用三个部门实例的方法来实现用餐功能 String food = pay.pay(); food = cook.cook(food); waiter.serve(food);
门面模式
*public**class* Facade { // 定义被Facade封装的三个部门 Payment pay; Cook cook; Waiter waiter; // 构造器 *public* Facade() { *this*.pay = *new* PaymentImpl(); *this*.cook = *new* CookImpl(); *this*.waiter = *new* WaiterImpl(); } *public**void* serveFood() { // 依次调用三个部门的方法,封装成一个serveFood()方法 String food = pay.pay(); food = cook.cook(food); waiter.serve(food); }}
门面模式调用
Facade f = *new* Facade(); f.serveFood();
3.8. 桥接模式(Bridge)
由于实际的需要,某个类具有两个以上的维度变化,如果只是使用继承将无法实现这种需要,或者使得设计变得相当臃肿。而桥接模式的做法是把变化部分抽象出来,使变化部分与主类分离开来,从而将多个的变化彻底分离。最后提供一个管理类来组合不同维度上的变化,通过这种组合来满足业务的需要。
Peppery口味风格接口:
*public**interface* Peppery{ String style();}
口味之一
*public**class* PepperySytle *implements* Peppery{ //实现"辣味"风格的方法 *public* String style() { *return*"辣味很重,很过瘾..."; }}
口味之二
*public**class* PlainStyle *implements* Peppery{ //实现"不辣"风格的方法 *public* String style() { *return*"味道清淡,很养胃..."; }}
口味的桥梁
*public**abstract**class* AbstractNoodle{ //组合一个Peppery变量,用于将该维度的变化独立出来 *protected* Peppery style; //每份Noodle必须组合一个Peppery对象 *public* AbstractNoodle(Peppery style) { *this*.style = style; } *public**abstract**void* eat();}
材料之一,继承口味
*public**class* PorkyNoodle *extends* AbstractNoodle{ *public* PorkyNoodle(Peppery style) { *super*(style); } //实现eat()抽象方法 *public**void* eat() { System.out.println("这是一碗稍嫌油腻的猪肉面条。" + *super*.style.style()); }}
材料之二,继承口味
*public**class* BeefMoodle *extends* AbstractNoodle{ *public* BeefMoodle(Peppery style) { *super*(style); } //实现eat()抽象方法 *public**void* eat() { System.out.println("这是一碗美味的牛肉面条。" + *super*.style.style()); }}
主程序
*public**class* Test{ *public**static**void* main(String[] args) { //下面将得到“辣味”的牛肉面 AbstractNoodle noodle1 = *new* BeefMoodle( *new* PepperySytle()); noodle1.eat(); //下面将得到“不辣”的牛肉面 AbstractNoodle noodle2 = *new* BeefMoodle( *new* PlainStyle()); noodle2.eat(); //下面将得到“辣味”的猪肉面 AbstractNoodle noodle3 = *new* PorkyNoodle( *new* PepperySytle()); noodle3.eat(); //下面将得到“不辣”的猪肉面 AbstractNoodle noodle4 = *new* PorkyNoodle( *new* PlainStyle()); noodle4.eat(); }}
Java EE应用中常见的DAO模式正是桥接模式的应用。
实际上,一个设计优良的项目,本身就是设计模式最好的教科书,例如Spring框架,当你深入阅读其源代码时,你会发现这个框架处处充满了设计模式的应用场景。
http://www.cnblogs.com/liuling/archive/2013/04/20/observer.html
3.9. 观察者模式(Observer)
观察者模式结构中包括四种角色:
一、主题:主题是一个接口,该接口规定了具体主题需要实现的方法,比如添加、删除观察者以及通知观察者更新数据的方法。
二、观察者:观察者也是一个接口,该接口规定了具体观察者用来更新数据的方法。
三、具体主题:具体主题是一个实现主题接口的类,该类包含了会经常发生变化的数据。而且还有一个集合,该集合存放的是观察者的引用。
四:具体观察者:具体观察者是实现了观察者接口的一个类。具体观察者包含有可以存放具体主题引用的主题接口变量,以便具体观察者让具体主题将自己的引用添加到具体主题的集合中,让自己成为它的观察者,或者让这个具体主题将自己从具体主题的集合中删除,使自己不在时它的观察者。
观察者模式定义了对象间的一对多依赖关系,让一个或多个观察者对象观察一个主题对象。当主题对象的状态发生变化时,系统能通知所有的依赖于此对象的观察者对象,从而使得观察者对象能够自动更新。
在观察者模式中,被观察的对象常常也被称为目标或主题(Subject),依赖的对象被称为观察者(Observer)。
Observer,观察者接口:
观察者:观察者也是一个接口,该接口规定了具体观察者用来更新数据的方法
*public**interface* Observer { *void* update(Observable o, Object arg);}
Observable,目标或主题:
主题:主题是一个接口,该接口规定了具体主题需要实现的方法,比如添加、删除观察者以及通知观察者更新数据的方法
*import* java.util.ArrayList;*import* java.util.List;*import* java.util.Iterator; *public**abstract**class* Observable { // 用一个List来保存该对象上所有绑定的事件监听器 List<Observer> observers = *new* ArrayList<Observer>(); // 定义一个方法,用于从该主题上注册观察者 *public**void* registObserver(Observer o) { observers.add(o); } // 定义一个方法,用于从该主题中删除观察者 *public**void* removeObserver(Observer o) { observers.add(o); } // 通知该主题上注册的所有观察者 *public**void* notifyObservers(Object value) { // 遍历注册到该被观察者上的所有观察者 *for* (Iterator it = observers.iterator(); it.hasNext();) { Observer o = (Observer) it.next(); // 显式每个观察者的update方法 o.update(*this*, value); } }}
Product被观察类:
具体主题:具体主题是一个实现主题接口的类,该类包含了会经常发生变化的数据。而且还有一个集合,该集合存放的是观察者的引用。
*public**class* Product *extends* Observable { // 定义两个属性 *private* String name; *private**double*price; // 无参数的构造器 *public* Product() { } *public* Product(String name, *double* price) { *this*.name = name; *this*.price = price; } *public* String getName() { *return*name; } // 当程序调用name的setter方法来修改Product的name属性时 // 程序自然触发该对象上注册的所有观察者 *public**void* setName(String name) { *this*.name = name; notifyObservers(name); } *public**double* getPrice() { *return*price; } // 当程序调用price的setter方法来修改Product的price属性时 // 程序自然触发该对象上注册的所有观察者 *public**void* setPrice(*double* price) { *this*.price = price; notifyObservers(price); }}
具体观察者:具体观察者是实现了观察者接口的一个类。具体观察者包含有可以存放具体主题引用的主题接口变量,以便具体观察者让具体主题将自己的引用添加到具体主题的集合中,让自己成为它的观察者,或者让这个具体主题将自己从具体主题的集合中删除,使自己不在时它的观察者。
NameObserver名称观察者:
*import* javax.swing.JFrame;*import* javax.swing.JLabel; *public**class* NameObserver *implements* Observer { // 实现观察者必须实现的update方法 *public**void* update(Observable o, Object arg) { *if* (arg *instanceof* String) { // 产品名称改变值在name中 String name = (String) arg; // 启动一个JFrame窗口来显示被观察对象的状态改变 JFrame f = *new* JFrame("观察者"); JLabel l = *new* JLabel("名称改变为:" + name); f.add(l); f.pack(); f.setVisible(*true*); System.out.println("名称观察者:" + o + "物品名称已经改变为: " + name); } }}
PriceObserver价格观察者:
*public**class* PriceObserver *implements* Observer { // 实现观察者必须实现的update方法 *public**void* update(Observable o, Object arg) { *if* (arg *instanceof* Double) { System.out.println("价格观察者:" + o + "物品价格已经改变为: " + arg); } }}
测试:
*public**class* Test { *public**static**void* main(String[] args) { // 创建一个被观察者对象 Product p = *new* Product("电视机", 176); // 创建两个观察者对象 NameObserver no = *new* NameObserver(); PriceObserver po = *new* PriceObserver(); // 向被观察对象上注册两个观察者对象 p.registObserver(no); p.registObserver(po); // 程序调用setter方法来改变Product的name和price属性 p.setName("书桌"); p.setPrice(345f); }}
其中Java工具类提供了被观察者抽象基类:java.util.Observable。观察者接口:java.util.Observer。
我们可以把观察者接口理解成事件监听接口,而被观察者对象也可当成事件源处理——换个角度来思考:监听,观察,这两个词语之间有本质的区别吗?Java事件机制的底层实现,本身就是通过观察者模式来实现的。除此之外,主题/订阅模式下的JMS本身就是观察者模式的应用。
单例模式
简单点说,就是一个应用程序中,某个类的实例对象只有一个,你没有办法去new,因为构造器是被private修饰的,一般通过getInstance()的方法来获取它们的实例。
getInstance()的返回值是一个对象的引用,并不是一个新的实例,所以不要错误的理解成多个对象。单例模式实现起来也很容易,直接看demo吧
public class Singleton { private static Singleton singleton; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
按照我的习惯,我恨不得写满注释,怕你们看不懂,但是这个代码实在太简单了,所以我没写任何注释,如果这几行代码你都看不明白的话,那你可以洗洗睡了,等你睡醒了再来看我的博客说不定能看懂。
上面的是最基本的写法,也叫懒汉写法(线程不安全)下面我再公布几种单例模式的写法:
懒汉式写法(线程安全)
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized 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; } }
静态内部类
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
枚举
public enum Singleton { INSTANCE; public void whateverMethod() { } }
这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏。
双重校验锁
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; } }
总结:我个人比较喜欢静态内部类写法和饿汉式写法,其实这两种写法能够应付绝大多数情况了。其他写法也可以选择,主要还是看业务需求吧。
观察者模式
对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
观察者模式UML图
看不懂图的人端着小板凳到这里来,给你举个栗子:假设有三个人,小美(女,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("你们过来吧,谁先过来谁就能陪我一起玩儿游戏!"); } } }
我们写一个测试类来看一下结果对不对
public 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流就使用了装饰者模式,用户在使用的时候,可以任意组装,达到自己想要的效果。 举个栗子,我想吃三明治,首先我需要一根大大的香肠,我喜欢吃奶油,在香肠上面加一点奶油,再放一点蔬菜,最后再用两片面包夹一下,很丰盛的一顿午饭,营养又健康。(ps:不知道上海哪里有卖好吃的三明治的,求推荐~)那我们应该怎么来写代码呢? 首先,我们需要写一个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()); } }
看到没有,一层一层封装,我们从里往外看:最里面我new了一个香肠,在香肠的外面我包裹了一层奶油,在奶油的外面我又加了一层蔬菜,最外面我放的是面包,是不是很形象,哈哈~ 这个设计模式简直跟现实生活中一摸一样,看懂了吗? 我们看看运行结果吧
运行结果
一个三明治就做好了~
适配器模式
** **
将两种完全不同的事物联系到一起,就像现实生活中的变压器。假设一个手机充电器需要的电压是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(); } }
运行结果如下:
计算机网络
Http和Https的区别
Http协议运行在TCP之上,明文传输,客户端与服务器端都无法验证对方的身份;Https是身披SSL(Secure Socket Layer)外壳的Http,运行于SSL上,SSL运行于TCP之上,是添加了加密和认证机制的HTTP。二者之间存在如下不同:
-
端口不同:Http与Http使用不同的连接方式,用的端口也不一样,前者是80,后者是443;
-
资源消耗:和HTTP通信相比,Https通信会由于加减密处理消耗更多的CPU和内存资源;
-
开销:Https通信需要证书,而证书一般需要向认证机构购买; Https的加密机制是一种共享密钥加密和公开密钥加密并用的混合加密机制。
Get与POST的区别
GET与POST是我们常用的两种HTTP Method,二者之间的区别主要包括如下五个方面:
(1). 从功能上讲,GET一般用来从服务器上获取资源,POST一般用来更新服务器上的资源;
(2). 从REST服务角度上说,GET是幂等的,即读取同一个资源,总是得到相同的数据,而POST不是幂等的,因为每次请求对资源的改变并不是相同的;进一步地,GET不会改变服务器上的资源,而POST会对服务器资源进行改变;
(3). 从请求参数形式上看,GET请求的数据会附在URL之后,即将请求数据放置在HTTP报文的 请求头 中,以?分割URL和传输数据,参数之间以&相连。特别地,如果数据是英文字母/数字,原样发送;否则,会将其编码为 application/x-www-form-urlencoded MIME 字符串(如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密,得出如:%E4%BD%A0%E5%A5%BD,其中%XX中的XX为该符号以16进制表示的ASCII);而POST请求会把提交的数据则放置在是HTTP请求报文的 请求体 中。
(4). 就安全性而言,POST的安全性要比GET的安全性高,因为GET请求提交的数据将明文出现在URL上,而且POST请求参数则被包装到请求体中,相对更安全。
(5). 从请求的大小看,GET请求的长度受限于浏览器或服务器对URL长度的限制,允许发送的数据量比较小,而POST请求则是没有大小限制的。
OSI网络体系结构与TCP/IP协议模型
1). 物理层
参考模型的最低层,也是OSI模型的第一层,实现了相邻计算机节点之间比特流的透明传送,并尽可能地屏蔽掉具体传输介质和物理设备的差异,使其上层(数据链路层)不必关心网络的具体传输介质。
2). 数据链路层(data link layer)
接收来自物理层的位流形式的数据,并封装成帧,传送到上一层;同样,也将来自上层的数据帧,拆装为位流形式的数据转发到物理层。这一层在物理层提供的比特流的基础上,通过差错控制、流量控制方法,使有差错的物理线路变为无差错的数据链路,即提供可靠的通过物理介质传输数据的方法。
3). 网络层
将网络地址翻译成对应的物理地址,并通过路由选择算法为分组通过通信子网选择最适当的路径。
4). 传输层(transport layer)
在源端与目的端之间提供可靠的透明数据传输,使上层服务用户不必关系通信子网的实现细节。在协议栈中,传输层位于网络层之上,传输层协议为不同主机上运行的进程提供逻辑通信,而网络层协议为不同主机提供逻辑通信,如下图所示。
实际上,网络层可以看作是传输层的一部分,其为传输层提供服务。但对于终端系统而言,网络层对它们而言是透明的,它们知道传输层的存在,也就是说,在逻辑上它们认为是传输层为它们提供了端对端的通信,这也是分层思想的妙处。
5). 会话层(Session Layer)
会话层是OSI模型的第五层,是用户应用程序和网络之间的接口,负责在网络中的两节点之间建立、维持和终止通信。
6). 表示层(Presentation Layer):数据的编码,压缩和解压缩,数据的加密和解密
表示层是OSI模型的第六层,它对来自应用层的命令和数据进行解释,以确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。
7). 应用层(Application layer):为用户的应用进程提供网络通信服务
冯诺依曼:
(1)计算机处理的数据和指令一律用二进制数表示
(2)顺序执行程序
计算机运行过程中,把要执行的程序和处理的数据首先存入主存储器(内存),计算机执行程序时,将自动地并按顺序从主存储器中取出指令一条一条地执行,这一概念称作顺序执行程序。
(3)计算机硬件由运算器、控制器、存储器、输入设备和输出设备五大部分组成。
IP地址与物理地址
物理地址是数据链路层和物理层使用的地址,IP地址是网络层和以上各层使用的地址,是一种逻辑地址,其中ARP协议用于IP地址与物理地址的对应。
这篇关于java面试题及答案的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-27消息中间件底层原理资料详解
- 2024-11-27RocketMQ底层原理资料详解:新手入门教程
- 2024-11-27MQ底层原理资料详解:新手入门教程
- 2024-11-27MQ项目开发资料入门教程
- 2024-11-27RocketMQ源码资料详解:新手入门教程
- 2024-11-27本地多文件上传简易教程
- 2024-11-26消息中间件源码剖析教程
- 2024-11-26JAVA语音识别项目资料的收集与应用
- 2024-11-26Java语音识别项目资料:入门级教程与实战指南
- 2024-11-26SpringAI:Java 开发的智能新利器