Java面试问题整理

多线程

创建线程的三种方式

  1. 继承Thread类创建线程类
  2. 实现Runnable接口创建线程类
  3. 实现Callable接口创建线程类

线程池

  1. 四种线程池:

    • newCachedThreadPool:线程池容量大小为Integer.MAX_VALUE,当执行第二个任务时,若第一个任务已经完成,则会复用第一个线程。队列使用SynchronousQueue。
    • newFixedThreadPool:定长线程池,创建时确定容量,任务数超出线程数时将在队列中等待。队列使用LinkedBlockingQueue。
    • newScheduledThreadPool:定长线程池,且支持定时及周期性任务执行。队列使用DelayedWorkQueue。
    • newSingleThreadExecutor:单线程,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。
  2. corePoolSize:核心池大小,一般意义上的线程池容量。
  3. maximumPoolSize:当线程池中线程数量达到核心池大小后,仍有新任务到达系统将检查是否达到了maximumPoolSize,如果未达到,则新建线程以执行任务,如果已达到,则任务将在队列中等待或被拒绝执行。这部分新建的线程在执行完成后空闲时将被销毁。
  4. keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0。
  5. workQueue:一个阻塞队列,用来存储等待执行的任务。
  6. execute方法:向线程池提交一个任务,交由线程池执行。
  7. submit方法:向线程池提交一个任务,交由线程池执行并能返回结果,实际仍然是调用了execute方法但使用了Future来获取返回值。
  8. Worker内部类:

    1. 基本成员:thread和firstTask
    2. 工作循环:线程池中的线程复用是由工作循环实现的,Worker类实现了Runnable接口,它的run()方法就是一个无限循环,负责不断地从workQueue获得并执行任务。

Runnable

使用Thread进行多线程操作的时候,将耗时操作写在其中,在执行Thread的start方法时,将执行Runnable中的run方法。Runnable不支持返回值。

Callable

与Runnable功能类似,支持返回值。

Future

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果、设置结果操作。get方法会阻塞,直到任务返回结果。

FutureTask

由于FutureTask继承了Runnable,因此它既可以通过Thread包装来直接执行,也可以提交给ExecuteService来执行。并且还可以直接通过get()函数获取执行结果,该函数会阻塞,直到结果返回。因此FutureTask既是Future、Runnable,又是包装了Callable(如果是Runnable最终也会被转换为Callable),它是这两者的合体。

wait

暂时使方法进入wait状态,并让出对象的资源锁,使得其他线程可以使用对象的资源锁,只有调用notify()方法才能解除wait状态,重新参与到资源锁的竞争中去。wait()方法只能在同步方法或同步块中使用。

sleep

使线程进入sleep状态,但不会释放资源锁,在指定时间后线程恢复运行。sleep()方法在任何地方都可以使用。

线程安全

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。

synchronized

它包括两种用法:synchronized 方法和 synchronized 块。

  • synchronized方法:持有实例对象或类对象(类方法)对应的锁,只有方法执行完成后才会释放锁,所以多个synchronized方法在同一时间只有一个能执行。
  • synchronized块:持有synchronized(A){}中A的引用对应的锁。

Lock

Lock是一个接口类,主要实现有ReentrantLock, Condition, ReadWriteLock等。Lock接口主要有下面一些方法:

  • lock() :在允许的情况下锁住Lock实例。如果Lock实例已经被锁住,调用lock()方法的线程将会被阻塞住,直到Lock释放锁。
  • lockInterruptibly() :也会锁住Lock,除非调用了线程了interrupted方法。此外,如果一个线程正在通过该方法等待上锁,如果此时线程被中断,线程将退出这个方法的调用。lockInterruptibly()允许在等待时由其他线程的Thread.interrupt()方法来中断等待线程而直接返回,这时是不用获取锁的,而会抛出一个InterruptException。
  • tryLock() :尝试立即锁住Lock实例。如果锁成功会返回true,如果已经锁住了会返回false,这个方法不会被阻塞。
  • tryLock(long timeout, TimeUnit timeUnit) :与trywork()相似,只是可以设置一个等待加锁的超时时间。
  • unlock():方法解锁Lock实例。通常情况下,Lock的实现仅仅允许加锁的线程调用次方法,否则可能会导致非检查异常(RuntimeException)。

ReentrantLock

它拥有与synchronized相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性,此外,它还提供了在激烈争用情况下更佳的性能(1.6之后synchronized得到了性能优化,目前的JDK版本上性能接近)。它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。

ReadWriteLock

ReadWriteLock是一个接口,Java.util.concurrent.locks包中包含对ReadWriteLock的具体实现ReentrantReadWriteLock。

  • Read Lock:如果没有写入线程锁住ReadWriteLock,并且没有线程需要获得写入锁进行写入操作。那么多个线程可以获得锁来进行读操作。
  • Write Lock:如果没有线程在写或者读操作,那么一次仅有一个线程可以获得锁以进行写操作。

CAS

全称为Compare And Swap,CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

分布式环境下的并发问题解决方法

  • 避免并发:通过合理的时间调度,避免共享资源的存取冲突,或通过适当的任务设计策略,使得任务之间不存在共享资源,如通过Hash算法使得一个用户的数据总是被同一个工作机完成。
  • 时间戳:对于分布式环境下对数据值的更新,可以设置统一的时间戳服务,之后在更新时带上一个时间戳标志量,只有时间戳大于存在的时间戳才更新值。
  • 串行化:对于部分分布式操作无法保证时序性的情况,可以变分布式为单一系统来牺牲扩展性和性能来满足数据一致性的要求。
  • 数据库:通过数据库的高可靠一致性机制来保证共享数据库资源的一致性,如通过一条sql完成更新操作。
  • 行锁:对于复杂事务无法通过一条sql解决问题的,可以使用数据库的行锁或设置锁字段。
  • 统一触发途径:当一个数据可能会被多个触发点或多个业务涉及到,就有并发问题产生的隐患,因此可以通过前期架构和业务设计,尽量统一触发途径,触发途径少了一是减少并发的可能,也有利于对于并发问题的分析和判断。

集合

List

ArrayList

  1. 内部实现:数组
  2. 默认大小:10
  3. 扩容策略:扩容50%
  4. 线程安全:否

Vector

  1. 内部实现:数组
  2. 默认大小:10
  3. 扩容策略:扩容100%
  4. 线程安全:是

LinkedList

  1. 内部实现:双向链表
  2. 容量:理论上无限制
  3. 线程安全:否

Map

HashMap

  1. 内部实现:Node数组
  2. Hash冲突时解决:以链表的形式存储各Hash值相同的结点(Node存储),当某一数组元素包含的结点数达到8以上时,将链表转换为红黑树,结点存储结构变为TreeNode
  3. 默认数组大小:16
  4. 默认扩容比例:0.75
  5. 扩容策略:扩容100%
  6. 线程安全:否
  7. Null兼容性:key可有一个null,value可为null

HashTable

  1. 内部实现:Entry(继承自Map.Entry)数组
  2. Hash冲突时解决:以链表的形式存储各Hash值相同的结点(Node存储),当某一数组元素包含的结点数达到8以上时,将链表转换为红黑树,结点存储结构变为TreeNode
  3. 默认数组大小:11
  4. 默认扩容比例:0.75
  5. 扩容策略:扩容100%
  6. 线程安全:是
  7. 线程安全实现方法:synchronized
  8. Null兼容性:key和value均不可为null

ConcurrentHashMap

  1. 内部实现:Node数组(不允许setValue)
  2. Hash冲突时解决:以链表的形式存储各Hash值相同的结点(Node存储),当某一数组元素包含的结点数达到8以上时,将链表转换为红黑树,结点存储结构变为TreeNode
  3. 默认数组大小:16
  4. 默认扩容比例:0.75
  5. 扩容策略:扩容100%
  6. 线程安全:是
  7. 线程安全实现方法:针对元素加锁(Java 8);分段加锁(Java 8前)
  8. Null兼容性:key和value均不可为null

LinkedHashMap

  1. 内部实现:Entry(继承自HashMap.Node)链表
  2. 容量:理论上无限制
  3. 线程安全:否
  4. Null兼容性:key可有一个null,value可为null

TreeMap

  1. 内部实现:红黑树
  2. 容量:理论上无限制
  3. 线程安全:否
  4. Null兼容性:key不可为null,value可为null

Set

HashSet

  1. 内部实现:HashMap

LinkedHashSet

  1. 内部实现:LinkedHashMap

Queue

LinkedList

通过实现Queue接口可以作为队列使用,并限制了LinkedList的部分方法

ArrayQueue

  1. 内部实现:循环数组
  2. 默认大小:16
  3. 容量特点:2的power
  4. 扩容策略:扩容100%

PriorityQueue

  1. 内部实现:平衡二叉最小堆
  2. 排序依据:元素实现的IComparable接口或Comparator,值越小,优先级越高,越先出队

SynchronousQueue

同步队列,要求必须等待消费者取出元素后才能放入新元素。

BlockingQueue

当队列为空时,出队操作将会阻塞,直到有新元素入队;当队列已满时,入队操作将会阻塞,保证生产者和消费者的速度不会相差太远。

  • ArrayBlockingQueue:定长,基于循环数组实现,有一把公共的锁,通过notEmpty和notFull两个状态管理队列满或空时的阻塞状态。
  • LinkedBlockingQueue:可选定长,基于链表实现,可通过设置长度为Integer.MAX_VALUE成为无界的,入队无阻塞出队为空时阻塞的队列。
  • PriorityBlockingQueue:无界的优先队列,基于平衡二叉最小堆,有一把公共的锁,入队无阻塞出队为空时阻塞。
  • DelayQueue:与PriorityBlockingQueue类似,但实现了Delayed接口,在出队时可指定多长时间后返回。

JVM

Java内存模型

  • 主内存:主要包括方法区和堆。
  • 工作内存:每个线程拥有一个工作内存,主要包括属于该线程私有的栈和对主内存部分变量拷贝的寄存器。

垃圾收集机制

  1. “垃圾识别”算法:目前Oracle JDK和OpenJDK采用的都是可达性分析算法。
  2. 新生代和老年代

    • 新生代:分为Eden空间和Survival空间,对象一般在Eden空间中分配。Survival空间存放的是在Minor GC中存活的对象,每经历一次Minor GC对象的年龄增长1。
    • 老年代:大对象会直接在老年代中分配,Survival空间中年龄超过阈值或相同年龄的对象大小总和大于Survivor空间的一半,会被移动到老年代。
  3. 垃圾收集类别:

    • Minor GC/Young GC:当新生代中的Eden空间没有足够空间来分配新对象时触发,对于被识别为垃圾的对象将被清理,存活的对象会被移动到Survival空间。
    • Full GC/Major GC:当老年代中的空间不足时触发。
  4. 空间分配担保:新生代采用的是复制收集算法,Eden和Survivor始终只是用其中一块内存区,当出现Minor GC后大部分对象仍然存活的话,就需要老年代进行分配担保,把Survivor区无法容纳的对象直接晋升到老年代。
  5. Minor GC是否安全:在进行Minor GC之前,虚拟机会先检查老年代的最大可用连续空间是否大于新生代所有对象总空间,如果大于,则此次Minor GC是安全的;否则则要检查HandlePromotionFailure参数是否允许担保失败,如果允许,则会继续检查老年代的最大可用连续空间是否大于历次进入老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC;如果小于,或者不允许担保失败,则进行一次Full GC;如果尝试进行的Minor GC失败了,也要进行一次Full GC。

垃圾收集算法

  1. “标记-清除”算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点:效率低;产生大量内存碎片。
    “标记-清除”算法示意图
  2. “复制”算法:将可用内存划分为大小相等的两块,每次只使用其中的一块,当进行垃圾回收时,将存活的对象复制到另一块上,再把原来的空间一次清理掉。优点:实现简单,运行高效。缺点:将内存缩小了一半,代价高昂;存活率较高时复制操作增加,效率降低。
    “复制”算法示意图

Hotspot VM对新生代的垃圾回收算法采用的是复制算法,但没有将两块内存划分为大小相等,而是分为Edenh和Survivor,比例为8:1,当Survivor空间不够用时,需要依赖其他内存进行分配担保。

  1. “标记-整理”算法:与“标记-清除”算法一样,都是先标记出所有需要回收的对象,但后续不是直接清理可回收的对象,而是让存活的对象向一端移动,然后清理掉端边界以外的内存。
    “标记-整理”算法示意图
  2. 分代收集算法:针对对象存活周期的不同将内存划分为若干块,对每一块采用合适的算法去进行垃圾回收。如新生代的存活率较低,则采用复制算法;老年代存活率较高,且没有额外内存提供分配担保,必须采用“标记-清除”或“标记-整理”算法。

类加载的过程

加载

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生存一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

验证

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。注意对于静态变量设置初始值不是设置类变量被赋予的值,基本数据类型为对应的零值,引用类型为null;对于常量(即带有final修饰符的静态变量)则会直接设置其被赋予的值。

解析

解析阶段是将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符7类符号引用进行。

  • 符号引用:符号引用以一组符号来描述所引用的模板,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关。
  • 直接引用:直接饮用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的。

初始化

初始化阶段是执行类构造器<clinit>()方法的过程(注意与<init>()方法区分)。

关于 <clinit>() 方法

  1. <clinit>()方法由所有类变量的赋值动作和静态语句块合并产生的,顺序由语句在源文件中出现的顺序所决定的,静态语句块只能访问在静态语句块之前的变量,定义在后面的变量,静态语句块内可以赋值但不能访问。
  2. 虚拟机会保证先调用父类的<clinit>()方法,所以在虚拟机中第一个被执行的<clinit>()方法的类是java.lang.Object,也意味着父类中定义的静态语句块要优先于子类。
  3. 接口也可能会生成<clinit>()方法,因为接口可能存在变量初始化的赋值操作。接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有父接口中定义的变量被使用时,父接口才会初始化。
  4. <clinit>()方法对于类或接口不是必需的,如果他们没有静态语句块和静态变量的赋值操作,那么编译器可以不为他们生成<clinit>()方法。
  5. 虚拟机会保证<clinit>()方法是线程安全的。

类的主动引用

对于类的初始化阶段,虚拟机规范中严格规定了有且仅有以下5种情况必须立即对类进行初始化,这5种情况中的行为称为对一个类进行主动引用。除此以外,所有引用类的方法都不会触发初始化,称为被动引用。

  1. 遇到new, getstatic, putstaticinvokestatic这4条字节码指令时,如果类尚未进行过初始化,则先触发类的初始化。如使用new关键字实例化对象,读取或设置一个类的静态字段(非final),调用一个类的静态方法。
  2. 使用java.lang.reflect包方法对类进行反射调用时。如果该类尚未进行过初始化,则先触发类的初始化。
  3. 初始化一个类时,如果该类的父类尚未进行过初始化,则先触发其父类的初始化。
  4. 虚拟机启动时,主类会被首先初始化。
  5. 使用JDK 1.7以上的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStatic, REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类尚未进行过初始化,则需要先触发类的初始化。

类加载器

类加载器用于实现类的加载动作。比较两个类是否相等,只有在由同一个类加载器加载的前提下才有意义,对于使用equals()方法,isAssignableFrom()方法,isInstance()方法和instanceof关键字的结果都有影响。

  • 启动类加载器(Bootstrap ClassLoader):负责加载存放在JAVA_HOME/lib目录中或被-Xbootclasspath参数所指定的路径的,并且被虚拟机识别的类库。
  • 扩展类加载器(Extension ClassLoader):负责加载JAVA_HOME/lib/ext目录中或者被java.ext.dirs系统变量锁指定的路径中的类库。
  • 应用程序类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)上所指定的类库。

双亲委派模型

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的父子关系一般是以组合关系而不是继承关系来实现。

类加载器的双亲委派模型

双亲委派模型的工作过程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈无法完成加载请求时,子加载器才会尝试自己去加载。

类加载安全机制

如果用户定义了以java.xxx开头的包,类加载器加载时将抛出SecurityException,而根据双亲委派模型的工作过程来看,也不能自定义类似java.lang.String的类,因为系统类在虚拟机启动时已经完成了加载,不会加载全限定名相同的类。

排序算法

快速排序

  1. 选出数据列中的一个值K(通常是第一个);
  2. 从后往前找到第一个比K小的值(必须先从后往前);
  3. 从前往后找到第一个比K大的值;
  4. 交换2,3找到的值;
  5. 重复2-4直到相遇,交换相遇点和K;
  6. 从相遇点分为左右两个子数据列,重复以上过程直到数据列不可再分。

堆排序

看这里吧

设计模式

单例

  • 单线程单例:私有构造方法,并通过getInstance方法获取对象实例,非线程安全,仅在单线程应用中可用。
  • 懒汉式单例:在getInstance中进行非null判断以此决定是否实例化对象,并在方法上增加synchronized关键字保证线程安全,但由于对整个方法进行了同步,故效率很低。
  • 饿汉式单例:在类初始化时完成对象的实例化,线程安全,但没有实现懒加载,内存使用率低。
  • 双重校验锁:在getInstance方法中进行一次非null判断后,实例化代码使用同步锁保证线程安全,并在同步块内再次判断非null,避免重复实例化,无需实例化的情况不需要加锁,提高了效率,线程安全,实现了懒加载,但实现较复杂,容易出错。
public class Singleton {
    private static Singleton instance=null;
    private Singleton(){
        
    }
    public static Singleton getInstance(){
        if(instance==null){
            synchronized(Singleton.class){
                if(instance==null){
                    instance=new Singleton();
                }
            }
        }
        return instance;
    }
}
  • 静态内部类:在需要实现单例类的内部使用一个静态类,并将内部类使用饿汉式初始化单例对象,线程安全,且实现了懒加载。
  • 枚举类:利用JVM保证枚举的构造方法只会被调用一次实现线程安全,即使被反序列化也不会重复构造对象,目前最安全的实现,但是写法不常见。
public class SingletonTest {
    
    private static enum EnumSingleton {
        INSTANCE;

        private SingletonTest singleton;
        private EnumSingleton() {
            singleton = new SingletonTest();
        }

        public SingletonTest getInstance() {
            return singleton;
        }
    }

    public SingletonTest getInstance() {
        return EnumSingleton.INSTANCE.getInstance();
    }

}

观察者

观察者模式中,一个被观察者管理所有相依于它的观察者物件,并且在本身的状态改变时主动发出通知。这通常通过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。

角色

  • 抽象被观察者角色:把所有对观察者对象的引用保存在一个集合中,每个被观察者角色都可以有任意数量的观察者。被观察者提供一个接口,可以增加和删除观察者角色。一般用一个抽象类和接口来实现。
  • 抽象观察者角色:为所有具体的观察者定义一个接口,在得到主题的通知时更新自己。
  • 具体被观察者角色:在被观察者内部状态改变时,给所有登记过的观察者发出通知。具体被观察者角色通常用一个子类实现。
  • 具体观察者角色:该角色实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。通常用一个子类实现。如果需要,具体观察者角色可以保存一个指向具体主题角色的引用。

适用场景

  1. 当一个抽象模型有两个方面, 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
  2. 当对一个对象的改变需要同时改变其它对象, 而不知道具体有多少对象有待改变。
  3. 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之, 你不希望这些对象是紧密耦合的。

Java 日期时间API

传统日期时间API

  1. java.util.Date:通过记录1970到其代表的时间的毫秒数来表示一个时间,主要有两个构造方法,一个通过调用System.currentTimeMillis()获取当前系统时间,另一个要求传入一个long类型的毫秒数表示的时间,其他构造方法已过时;常用的功能是比较时间的前后和获取时间的毫秒数。
  2. java.util.Calendar:内部也记录了时间的毫秒数,其实比较接近是java.util.Date的装饰者(装饰者模式),提供了大量的工具方法,常用的比如获取时间的年份,月份,日期等,或对日期时间进行加减,此外,还支持日期时间的国际化和时区。注意:月份参数从0开始。
  3. java.text.DateFormat:用于将日期时间对象转换成字符串,本身是抽象类,提供了简单的格式化功能。实际上一般使用java.text.SimpleDateFormat(模板模式),可以通过格式化字符串来自定义格式,如:"yyyy-MM-dd HH:mm:ss"。

Java 8 日期时间API

以下类均在java.time包下

  1. Clock

Clock类提供了访问当前日期和时间的方法。Clock使用时区来访问当前的instant, date和time。Clock类可以替换 System.currentTimeMillis() 和 TimeZone.getDefault()。


//Clock 时钟
Clock clock1 = Clock.systemDefaultZone();//获取系统默认时区 (当前瞬时时间 )
System.out.println( "系统时间日期:"+clock1.instant() );
System.out.println( "时间毫秒:"+clock1.millis() );
        
final Clock clock = Clock.systemUTC();//获取系统时钟,并将其转换成使用UTC时区的日期和时间
System.out.println( "时间日期:"+clock.instant() );
System.out.println( "时间毫秒值:"+clock.millis() );
  1. Instant

Instant类可以表示某一个特定的时间点,或用于创建一个传统的Date类。


Instant instant = clock1.instant();  
Date javadate = Date.from(instant);   
System.out.println( "date:"+javadate);
  1. ZoneId

在新API中时区使用ZoneId来表示。时区可以很方便的使用静态方法of()来获取到。时区定义了到UTC时间的时间差,在Instant时间点对象到本地日期对象之间转换的时候是极其重要的。


// 输出所有可见的时区ID,eg:Asia/Aden, America/Cuiaba, Etc/GMT+9等  
System.out.println(ZoneId.getAvailableZoneIds());  
  
ZoneId zone1 = ZoneId.of("Europe/Berlin");  
ZoneId zone2 = ZoneId.of("Brazil/East");  
System.out.println(zone1.getRules());  
System.out.println(zone2.getRules());  
//输出结果: ZoneRules[currentStandardOffset=+01:00]  
//输出结果: ZoneRules[currentStandardOffset=-03:00]
  1. LocalTime

LocalTime 定义了一个没有时区信息的时间。


// 获取当前本地时间
final LocalTime time = LocalTime.now();  
final LocalTime timeFromClock = LocalTime.now( clock );  
System.out.println( time );  
System.out.println( timeFromClock );  

// 获取指定时区时间
ZoneId zone1 = ZoneId.of("Europe/Berlin");  
ZoneId zone2 = ZoneId.of("Brazil/East");  
LocalTime now1 = LocalTime.now(zone1);  
LocalTime now2 = LocalTime.now(zone2);  
System.out.println("时区:Europe/Berlin---"+now1);   
System.out.println("时区:Brazil/East---"+now2); 

// 使用多种工厂方法创建LocalTime
LocalTime late = LocalTime.of(22, 12, 18);//时分秒  
System.out.println(late); // 输出结果:22:12:18  
DateTimeFormatter germanFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(Locale.GERMAN);  
LocalTime leetTime = LocalTime.parse("15:39", germanFormatter);  
System.out.println(leetTime); // 输出结果: 15:39 
  1. LocalDate

LocalDate 表示了一个确切的日期(eg: 2014-03-11)。该对象值是不可变的,使用方式和LocalTime基本一致。


Clock clock = Clock.systemDefaultZone();// 获取系统默认时区 (当前瞬时时间 )  
// 获取当前本地日期
final LocalDate date = LocalDate.now();  
final LocalDate dateFromClock = LocalDate.now(clock);  
System.out.println(date);  
System.out.println(dateFromClock); 

// 从字符串解析一个LocalDate类型
DateTimeFormatter germanFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.GERMAN);  
LocalDate xmas = LocalDate.parse("25.10.2016", germanFormatter);  
System.out.println(xmas);
  1. LocalDateTime

表示了具体时间和日期。LocalDateTime和LocalTime还有LocalDate一样,都是不可变的。LocalDateTime提供了一些能访问具体字段的方法。


Clock clock = Clock.systemDefaultZone();// 获取系统默认时区 (当前瞬时时间 )  
// 获取当前本地日期和时间
final LocalDateTime datetime = LocalDateTime.now();  
final LocalDateTime datetimeFromClock = LocalDateTime.now(clock);  
System.out.println(datetime);  
System.out.println(datetimeFromClock); 

// 工具方法
LocalDateTime sylvester = LocalDateTime.of(2016, Month.DECEMBER, 31, 23, 59, 59);  
       
DayOfWeek dayOfWeek = sylvester.getDayOfWeek();  
System.out.println(dayOfWeek);        
    
Month month = sylvester.getMonth();  
System.out.println(month);          
    
long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);  
System.out.println(minuteOfDay);

// 格式化时间
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM dd, yyyy - HH:mm");  
LocalDateTime parsed = LocalDateTime.parse("05 03, 2016 - 07:13", formatter);  
String string = formatter.format(parsed);  
System.out.println(string);
  1. ZonedDateTime

使用ZonedDateTime,它保存有ISO-8601日期系统的日期和时间,而且有时区信息。


Clock clock = Clock.systemDefaultZone();// 获取系统默认时区 (当前瞬时时间 )  
// 获取指定时区的日期时间 
final ZonedDateTime zonedDatetime = ZonedDateTime.now();  
final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now(clock);  
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now(ZoneId.of("America/Los_Angeles"));  
System.out.println(zonedDatetime);  
System.out.println(zonedDatetimeFromClock);  
System.out.println(zonedDatetimeFromZone);
  1. Duration

Duration持有的时间精确到纳秒。很容易计算两个日期中间的差异。


// 计算两个日期中间的差异
final LocalDateTime from = LocalDateTime.of(2014, Month.APRIL, 16, 0, 0, 0);//年月日时分秒  
final LocalDateTime to = LocalDateTime.of(2015, Month.APRIL, 16, 23, 59, 59);  
final Duration duration = Duration.between(from, to);  
System.out.println("Duration in days: " + duration.toDays());  
System.out.println("Duration in hours: " + duration.toHours());
  1. DateTimeFormatter

格式化日期时间。

// 格式化日期时间
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM dd, yyyy - HH:mm");  
LocalDateTime parsed = LocalDateTime.parse("05 03, 2016 - 07:13", formatter);  
String string = formatter.format(parsed);  
System.out.println(string);

传统和Java 8日期时间API比较

java.timejava.util.Datejava.util.Calendar
流畅的API不流畅的API
实例不可变实例可变
线程安全线程不安全

Java IO

  1. 字节流和字符流:

    • 字节流:以字节为单位,每次次读入或读出是8位数据。可以读任何类型数据。
    • 字符流:以字符为单位,每次次读入或读出是16位数据。其只能读取字符类型数据。
  2. 输出流和输入流:

    • 输出流:从内存读出到文件。只能进行写操作。
    • 输入流:从文件读入到内存。只能进行读操作。

注意:这里的出和入,都是相对于系统内存而言的。

  1. 节点流和处理流:

    • 节点流:直接与数据源相连,读入或读出。
    • 处理流:与节点流一块使用,在节点流的基础上,再套接一层,套接在节点流上的就是处理流。

为什么要有处理流?直接使用节点流,读写不方便,为了更快的读写文件,才有了处理流。

类结构图

参考知乎专栏文章

Java NIO

  1. Channel:Channel(通道)和IO中的流是差不多一个等级的。只不过流是单向的,譬如:InputStream, OutputStream.而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。NIO中的Channel的主要实现有:FileChannel,DatagramChannel,SocketChannel,ServerSocketChannel。分别可以对应文件IO、UDP和TCP(Server和Client)。
  2. Buffer:NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。
  3. Selectors:Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

参考1

参考2

数据库

SQL优化

  1. 请求了多余的数据:

    • 查询了不需要的记录:限制查询记录数量,使用LIMIT关键字
    • 查询了不需要的字段:慎重使用SELECT * 来查询所有字段,只查询需要的字段
    • 重复查询相同数据:考虑使用缓存,将需要重复查询的数据缓存下来
  2. 扫描了额外的记录:针对WHERE语句中的字段建立索引,慎重使用聚合函数和LIKE条件,避免出现全表扫描。
  3. 切分大型查询:对大量数据进行查询时,可切分为多个较小的查询,避免锁定的全表或过多的行导致阻塞其他查询。
  4. 分解关联查询:将关联查询分解为多个单表查询,提高缓存利用率,减少重复查询数据。
  5. MyISAM的神秘加成:由于MyISAM存储引擎的特点,可以几乎无成本地获得全表的行数,查询特定条件行数时,可以考虑使用全表行数减去不符合条件的行数。
  6. 优化关联查询:确保ON或者USING子句中的列上有索引,并且确保只在被关联表的列上有索引。
  7. 优化GROUP BY和DISTINCT:尽量在有索引的字段上使用GROUP BY和DISTINCT。

索引

索引的类型

  1. B-Tree索引:使用B-Tree数据结构存储数据(也可能是B+Tree或T-Tree),顺序组织存储,适合查找范围数据,适用于全键值,键值范围或键前缀(最左列)查找。
  2. Hash索引:基于哈希表实现,只有精确匹配索引所有列才有效。
  3. 全文索引:适用于全文搜索,如MATCH AGAINST操作。

索引的使用

  1. 独立的列:索引列不能是表达式的一部分,也不能是函数的参数。
  2. 前缀索引:对于BLOB,TEXT或过长的VARCHAR类型,可以考虑使用前缀索引来缩小索引大小和加快索引速度。
  3. 多列索引:尝试使用多列索引替代多个单列索引。
  4. 索引列顺序:创建多列索引时,优先将需要排序或分组的列排在前面,然后是选择性较高的列。

SQL关键字/子句的执行顺序

  1. from
  2. join
  3. on
  4. where
  5. group by(开始使用select中的别名,后面的语句中都可以使用)
  6. avg,sum....
  7. having
  8. select
  9. distinct
  10. order by

Explain包含哪些列

  1. id:SELECT识别符。
  2. select_type:select类型,它有以下几种值:

    • simple:表示不需要union操作或者不包含子查询的简单select查询。有连接查询时,外层的查询为simple,且只有一个。
    • primary:一个需要union操作或者含有子查询的select,位于最外层的单位查询的select_type即为primary。且只有一个。
    • subquery:除了from字句中包含的子查询外,其他地方出现的子查询都可能是subquery
    • dependent subquery:与dependent union类似,表示这个subquery的查询要受到外部表查询的影响。
    • derived:from字句中出现的子查询,也叫做派生表,其他数据库中可能叫做内联视图或嵌套select。
    • union:union连接的两个select查询,第一个查询是dervied派生表,除了第一个表外,第二个以后的表select_type都是union。
    • dependent union:与union一样,出现在union 或union all语句中,但是这个查询要受到外部查询的影响
    • union result:包含union的结果集,在union和union all语句中,因为它不需要参与查询,所以id字段为null。
  3. table:输出的行所用的表。
  4. type:连接类型,从最佳类型到最差类型:

    • system:表仅有一行,这是const类型的特例,平时不会出现,这个也可以忽略不计。
    • const:表最多有一个匹配行,const用于比较primary key或者unique索引。
    • eq_ref:对于每个来自于前面的表的行组合,从该表中读取一行。
    • ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取。
    • ref_or_null:该联接类型如同ref,但是添加了MySQL可以专门搜索包含NULL值的行。
    • index_merge:该联接类型表示使用了索引合并优化方法。
    • unique_subquery:
    • index_subquery:
    • range:给定范围内的检索,使用一个索引来检查行。
    • index:该联接类型与ALL相同,除了只有索引树被扫描。
    • ALL:对于每个来自于先前的表的行组合,进行完整的表扫描。
  5. possible_keys:提示使用哪个索引会在该表中找到行。
  6. key:MYSQL使用的索引。
  7. key_len:MYSQL使用的索引长度。
  8. ref:显示使用哪个列或常数与key一起从表中选择行。
  9. rows:显示MYSQL执行查询的行数。
  10. Extra:这个列可以显示的信息非常多,有几十种,常用的有:

    • distinct:在select部分使用了distinc关键字
    • no tables used:不带from字句的查询或者From dual查询。
    • using filesort:排序时无法使用到索引时,就会出现这个。常见于order by和group by语句中。
    • using index:查询时不需要回表查询,直接通过索引就可以获取查询的数据。
    • using_union:表示使用or连接各个使用索引的条件时,该信息表示从处理结果获取并集
    • using intersect:表示使用and的各个索引的条件时,该信息表示是从处理结果获取交集
    • using sort_union和using sort_intersection:与前面两个对应的类似,只是他们是出现在用and和or查询信息量大时,先查询主键,然后进行排序合并后,才能读取记录并返回。
    • using where:表示存储引擎返回的记录并不是所有的都满足查询条件,需要在server层进行过滤。查询条件中分为限制条件和检查条件,5.6之前,存储引擎只能根据限制条件扫描数据并返回,然后server层根据检查条件进行过滤再返回真正符合查询的数据。5.6.x之后支持ICP特性,可以把检查条件也下推到存储引擎层,不符合检查条件和限制条件的数据,直接不读取,这样就大大减少了存储引擎扫描的记录数量。extra列显示using index condition
    • using temporary:表示使用了临时表存储中间结果。临时表可以是内存临时表和磁盘临时表,执行计划中看不出来,需要查看status变量,used_tmp_table,used_tmp_disk_table才能看出来。
    • firstmatch(tb_name):5.6.x开始引入的优化子查询的新特性之一,常见于where字句含有in()类型的子查询。如果内表的数据量比较大,就可能出现这个
    • loosescan(m..n):5.6.x之后引入的优化子查询的新特性之一,在in()类型的子查询中,子查询返回的可能有重复记录时,就可能出现这个
    • filtered:使用explain extended时会出现这个列,5.7之后的版本默认就有这个字段,不需要使用explain extended了。这个字段表示存储引擎返回的数据在server层过滤后,剩下多少满足查询的记录数量的比例,注意是百分比,不是具体记录数。

Spring

请求处理流程

  1. 用户向服务器发送请求,请求被Spring 前端控制Servlet DispatcherServlet捕获;
  2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI)。然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain对象的形式返回;
  3. 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;
  7. ViewResolver 结合Model和View,来渲染视图;
  8. 将渲染结果返回给客户端。

定时任务


import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.concurrent.TimeUnit;
import org.joda.time.DateTime;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class TaskA {
       @Scheduled(cron="0/10 * * * * ? ")   //每10秒执行一次    
       @Override    
       public void doTask(){    
            try {
                TimeUnit.SECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
            System.out.println(sdf.format(DateTime.now().toDate())+" 任务每10秒执行一次");    
       }    
}  

Cron表达式

一个Cron表达式有至少6个(也可能7个)有空格分隔的时间元素。按顺序依次为

  1. 秒(0~59)
  2. 分钟(0~59)
  3. 小时(0~23)
  4. 天(0~31)
  5. 月(0~11)
  6. 星期(1~7 1=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT)
  7. 年份(1970-2099)

其中每个元素可以是一个值(如6),一个连续区间(9-12),一个间隔时间(8-18/4)(/表示每隔4小时),一个列表(1,3,5),通配符。由于"月份中的日期"和"星期中的日期"这两个元素互斥的,必须要对其中一个设置?.

Spring MVC和Struts2的区别

  1. 拦截级别:Spring MVC是基于方法级别的拦截,Struts2是基于类级别的拦截。
  2. Servlet变量作用域:Spring MVC是方法独享变量,Struts2是类内共享。
  3. 请求入口:Spring MVC是Servlet,Struts2是filter。
  4. AJAX支持:Spring MVC只需要@ResponseBody注解,Struts2需要自己写工具。
  5. Bean验证支持:Spring MVC支持JSR303
  6. 设计思想:Spring MVC在Servlet上扩展,Struts2更加符合OOP思想。

Spring用了哪些设计模式

  1. 简单工厂:BeanFactory
  2. 工厂方法:FactoryBean
  3. 单例:Spring管理的Bean默认为单例
  4. 适配器:AOP
  5. 装饰者:带有Wrapper或者Decorator的类
  6. 代理:AOP
  7. 观察者:Listener
  8. 策略:对象实例化
  9. 模板方法:JdbcTemplate

Spring中AOP主要用来做什么

  1. 日志记录
  2. 事务管理
  3. 权限管理

Spring注入bean的方式

  1. 构造方法注入
  2. Setter注入
  3. 字段注入

IOC与依赖注入

  • 控制反转(Inversion of Control),把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器。
  • 依赖注入(Dependency Injection),组件提供普通的Java方法让容器决定依赖关系,被管理的组件只需要暴露JavaBean的setter方法或者带参数的构造子或者接口,使容器可以在初始化时组装对象的依赖关系。
  • 依赖注入是实现控制反转的一种方式。

Spring是单例还是多例,怎么修改

单例,可通过@Scope注解修改

Spring事务隔离级别和传播性

事务隔离级别

  1. SERIALIZABLE,最严格的级别,事务串行执行,资源消耗最大。
  2. REPEATABLE_READ,保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。
  3. READ_COMMITTED,大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。
  4. READ_UNCOMMITTED,保证了读取过程中不会读取到非法数据。

另外,事务常用的两个属性:readonly,设置事务为只读以提升性能;timeout,设置事务的超时时间,一般用于防止大事务的发生。

事务隔离级别脏读不可重复读幻读
SERIALIZABLE不会不会不会
REPEATABLE_READ不会不会
READ_COMMITTED不会
READ_UNCOMMITTED

传播级别

  1. PROPAGATION_REQUIRED,默认的spring事务传播级别,使用该级别的特点是,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。
  2. PROPAGATION_SUPPORTS,从字面意思就知道,supports,支持,该传播级别的特点是,如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。所以说,并非所有的包在transactionTemplate.execute中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。
  3. PROPAGATION_MANDATORY,该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。
  4. PROPAGATION_REQUIRES_NEW,从字面即可知道,new,每次都要一个新事务,该传播级别的特点是,每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
  5. PROPAGATION_NOT_SUPPORTED,这个也可以从字面得知,not supported ,不支持,当前级别的特点就是上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。
  6. PROPAGATION_NEVER,该事务更严格,上面一个事务传播级别只是不支持而已,有事务就挂起,而PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!这个级别上辈子跟事务有仇。
  7. PROPAGATION_NESTED,字面也可知道,nested,嵌套级别事务。该传播级别特征是,如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

ORM

Hibernate实体对象四大状态

Hibernate定义了对象的以下四种状态:

  • 瞬时态(Transient):创建Session后,由 new 操作符创建,且尚未与Hibernate Session 关联的对象被认定为瞬时的。瞬时对象不会被持久化到数据库中,也不会被赋予持久化标识。 如果瞬时对象在程序中没有被引用,它会被垃圾回收器销毁。
  • 持久态(Persistent):持久的实例可能是刚被保存的,或刚被加载的,无论哪一种,按定义,它存在于相关联的Session作用范围内。 Hibernate会检测到处于持久状态的对象的任何改动,在当前操作单元(unit of work)执行完毕时将对象数据(state)与数据库同步,开发者不需要手动执行UPDATE。
  • 脱管态(Detached):也叫游离态,与持久对象关联的Session被关闭后,对象就变为脱管的。脱管态不能直接持久化,需要重新保存。
  • 删除态(DELETED):调用Session的delete方法之后,对象就变成删除态,此时Session中仍然保存有对象的信息,对删除态对象的引用依然有效,对象可继续被修改。删除态对象如果重新关联到某个新的 Session 上(也就是执行持久化操作), 会再次转变为持久的(在DELETED其间的改动将被持久化到数据库)。

Hibernate和Mybatis的区别

  1. 开发

    • hibernate开发中,sql语句已经被封装,直接可以使用,加快系统开发。
    • Mybatis 属于半自动化,sql需要手工完成,稍微繁琐。
  2. sql优化

    • Hibernate 自动生成sql,有些语句较为繁琐,会多消耗一些性能。
    • Mybatis 手动编写sql,可以避免不需要的查询,提高系统性能。
  3. 对象管理比对

    • Hibernate 是完整的对象-关系映射的框架,开发工程中,无需过多关注底层实现,只要去管理对象即可。
    • Mybatis 需要自行管理映射关系。
  4. 缓存

    • Hibernate的二级缓存配置在SessionFactory生成的配置文件中进行详细配置,然后再在具体的表-对象映射中配置是那种缓存。
    • MyBatis的二级缓存配置都是在每个具体的表-对象映射中进行详细配置,这样针对不同的表可以自定义不同的缓存机制。并且Mybatis可以在命名空间中共享相同的缓存配置和实例,通过Cache-ref来实现。

Hibernate优势

  1. Hibernate的DAO层开发比MyBatis简单,Mybatis需要维护SQL和结果映射。
  2. Hibernate对对象的维护和缓存要比MyBatis好,对增删改查的对象的维护要方便。
  3. Hibernate数据库移植性很好,MyBatis的数据库移植性不好,不同的数据库需要写不同SQL。
  4. Hibernate有更好的二级缓存机制,可以使用第三方缓存。MyBatis本身提供的缓存机制不佳。

Mybatis优势

  1. MyBatis可以进行更为细致的SQL优化,可以减少查询字段。
  2. MyBatis容易掌握,而Hibernate门槛较高。

Mybatis/Hibernate的缓存机制

相同点

Hibernate和Mybatis的二级缓存除了采用系统默认的缓存机制外,都可以通过实现你自己的缓存或为其他第三方缓存方案,创建适配器来完全覆盖缓存行为。

不同点

  • Hibernate的二级缓存配置在SessionFactory生成的配置文件中进行详细配置,然后再在具体的表-对象映射中配置是那种缓存。
  • MyBatis的二级缓存配置都是在每个具体的表-对象映射中进行详细配置,这样针对不同的表可以自定义不同的缓存机制。并且Mybatis可以在命名空间中共享相同的缓存配置和实例,通过Cache-ref来实现。

Hibernate具有良好的管理机制,用户不需要关注SQL,如果二级缓存出现脏数据,系统会保存;Mybatis 在使用的时候要谨慎,避免缓存Cache的使用。

Mybatis的mapper文件中#和$的区别

#相当于对数据 加上 双引号,$相当于直接显示数据

  1. #将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。如:order by #user_id#,如果传入的值是111,那么解析成sql时的值为order by "111",如果传入的值是id,则解析成的sql为order by "id"。
  2. $将传入的数据直接显示生成在sql中。如:order by $user_id$,如果传入的值是111,那么解析成sql时的值为order by user_id,如果传入的值是id,则解析成的sql为order by id。
  3. #方式能够很大程度防止sql注入。
  4. $方式无法防止Sql注入。
  5. $方式一般用于传入数据库对象,例如传入表名。
  6. 一般能用#的就别用$。

Mybatis的mapper文件中resultType和resultMap的区别

在MyBatis进行查询映射时,其实查询出来的每一个属性都是放在一个对应的Map里面的,其中键是属性名,值则是其对应的值。

  1. 当提供的返回类型属性是resultType时,MyBatis会将Map里面的键值对取出赋给resultType所指定的对象对应的属性。所以其实MyBatis的每一个查询映射的返回类型都是ResultMap,只是当提供的返回类型属性是resultType的时候,MyBatis对自动的给把对应的值赋给resultType所指定对象的属性。
  2. 当提供的返回类型是resultMap时,因为Map不能很好表示领域模型,就需要自己再进一步的把它转化为对应的对象,这常常在复杂查询中很有作用。

Mybatis的dao和mapper的方法是如何实现绑定的

在项目启动读取mybatis配置时,通过动态代理根据xml内容生成实现类。