JavaSE
-
包装类的比较 答案
1.由于常量池中的缓存,基本类型的包装类值在[-128,127] 期间,可以用
==
进行值比较(Double、Float是没有缓存的),不在该范围内的需要使用equals进行值比较;(对于包装类,无论何时强烈推荐都使用equals进行值比较)
2.基本类型的包装类,也可以手动拆箱转成基本类型(例如:intValue()),再使用==
进行值比较;
3.基本类型和基本类型的包装类之间的比较,可以直接使用==
运算符进行值比较(jdk使用了自动拆箱),也可以使用equals方法进行值比较(jdk编译器使用了自动装箱);
4.对于基本类型,由于不是对象,所以不存在地址,更没有equals等方法,故此只有使用“==”来进行值比较。使用==
运算符进行值比较; -
ArrayList与LinkedList的区别和特点:
1.ArrayList,与LinkedList都是属于实现了List接口的类。首先从名字前缀开始看 ,Array表示数组,Link表示链表。所以ArrayList底层是基于动态数组的。而LinkedList底层是基于双向链表的。
2.ArrayList必须是连续内存的,而LinkedList不要求连续内存。
3.ArrayList查询快,增加和删除慢;LinkedList增加和删除快,查询慢。
4.ArrayList 底层为动态数组,所以查询时是直接通过访问下标,查询效率高。而增加而删除时,为了保证内存的连续,增加和删除某一位置后,后方元素都得向前移动一位,
最坏情况就是删除第一个元素,则后面第2个到第n个元素都得往前移动一位。所以增加删除慢。
LinkedList底层为双向链表,不必保证内存上的连续,所以增删快,而查询时必须要经历从头到尾的遍历,所以查询慢。5.为什么说ArrayList是基于动态数组呢?一般的数组,容量确定了就不可以再更改,也无法超过。但是ArrayList可以,
例如当数组元素数已满时调用了add方法向尾部添加一个元素,则此时会进行扩容,ArrayList会自动创建一个更大的数组,并将所有元素拷贝到新数组中,而原数组会被抛弃6.会被GC回收。扩容后新数组的容量为原来的1.5倍。
-
i. 采用链地址法来解决冲突,即数组+链表来实现。主干是一个Entry数组,每一个Entry包含一个key-value键值对, 如果定位的数组位置不包含链表,那么当前的Entry指向null。 ii. 当发生哈希冲突且size大于阈值的时候,会对数组进行扩容,新建一个长度为之前2倍长的新数组, 然后将当前的Entry数组的元素传输过去。 iii. 重写equals方法的时候,必须重写HashCode方法。 iv. Jdk1.8以后加入红黑树优化,当链表长度超过8时,自动转换为红黑树。红黑树高度平均log(n), 最坏不超过2log(n),不是严格的平衡二叉树,但是查找复杂度等于平衡二叉树,加入节点时调整方式有 左旋、右旋、变色。性质:叶子节点一定是黑色,红色节点的子节点一定是黑色,根节点到叶子结点的 黑色节点数目相同。
-
hash冲突
- 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;
- 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。java.util.HashMap采用的链表法的方式,链表是单向链表。
-
static 关键字
Java静态代码块中的代码会在类加载JVM时运行,且只被执行一次,也就是说这些代码不需要实例化类就能够被调用。一般情况下,如果有些代码必须在项目启动的时候就执行的时候,就需要使用静态代码块。
Java静态代码块的用法:一个类可以使用不包含在任何方法体中的静态代码块,当类被载入时,静态代码块被执行,且只被执行一次,静态块常用来执行类属性的初始化。 -
hashmap的hash计算:
Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
return h & (length-1);
}
-
hashmap为什么用的是红黑树而不是二叉树和二叉平衡树?
-
ConcurrentHashMap
i. 锁的方式是细粒度的,他将hash表分为16个桶(默认值),只有在求size等操作时才需要锁定整个表;
ii. 使用的迭代方式,弱一致迭代器,在这种迭代方式中,迭代器被创建后,可以改变数据。 -
抽象类与接口的区别:
① 接口是抽象类的一种特例,接口中所有方法都必须是抽象的;
② 抽象类可以有构造方法,接口类不行;
③ 抽象类中可以包含非抽象的普通方法,接口类不行;
④ 抽象类中的抽象方法可以是protected,接口中只能是public;
⑤ 抽象类中可以包括静态方法,接口中不能;
⑥ 都可以包含静态成员变量,但是抽象类中的访问类型可以是任意的,而接口中只能是public static final类型。
⑦ 一个类可以实现多个接口,但是只能继承一个抽象类。 -
java8新特性 答案
-
hashcode 与 equals 重写的问题,两个中只有一个重写会产生的问题
主要原因是默认从Object继承来的hashCode是基于对象的ID实现的。 如果你重写了equals,比如说是基于对象的内容实现的,而保留hashCode的实现不变, 那么很可能某两个对象明明是“相等”,而hashCode却不一样。是 这样,当你用其中的一个作为键保存到hashMap、hasoTable或hashSet中,再以“相等的”找 另一个作为键值去查找他们的时候,则根本找不到。 资源:http://lelglin.iteye.com/blog/1826152 我们都知道Java语言是完全面向对象的,在java中,所有的对象都是继承于Object类。 Ojbect类中有两个方法equals、hashCode,这两个方法都是用来比较两个对象是否相等的。 在未重写equals方法我们是继承了object的equals方法,那里的 equals是比较两个对象的内存地址,显然我们new了2个对象内存地址肯定不一样 对于值对象,==比较的是两个对象的值 对于引用对象,比较的是两个对象的地址 默认的equals方法同==,一般来说我们的对象都是引用对象,要重写equals方法。 现在有一个学生对象,有属性学号跟姓名,现在我新建了一个学生对象,又从数据里查出一个 学生对象,这两个对象的学号跟姓名都一样,那这两个对象是不是相等呢?一般情况下,除非 你有特殊需求要处理,这两个对象是相等的,可如果用==去比较,返回的结果是错误的。 这时候我们就必须重写equlas方法了。如果学号是主键,在equals方法里,我们认为只要学号 相同,就可以返回true。 hashCode方法也是可以用来比较两个对象是否相等的。但是我们很少使用,应该说是很少直 接使用。hashCode方法返回的是一个int值,可以看做是一个对象的唯一编码,如果两个对象 的hashCode值相同,我们应该认为这两个对象是同一个对象。 一般如果使用java中的Map对象进行存储时,他会自动调用hashCode方法来比较两个对象是 否相等。 所以如果我们对equals方法进行了重写,建议一定要对hashCode方法重写,以保证相同的对 象返回相同的hash值,不同的对象返回不同的hash值。 如上面的学生例子,如果学号相同,不管姓名相不相同,返回的hash值一定要是一样的,这时 我们的hash值只与学号有关。
-
面向接口编程
因为接口可以避免类继承的所有问题。再说的严谨一点:JAVA语言中对接口的限制可以避免因类继承而引起的所有问题。
纵观类继承所引起的问题,都是由于其可被实例化造成的,而接口是不可被实例化的,所以其可以避免所有这些问题。由于其不能被实例化,所以不需要在其内部定义非static或非public的属性,进而导致定义非final的属性也是不恰当的(因为一个随时可被任何人随意修改的属性不符合面向对象的价值观);由于其不能被实例化,所以也不需要定义方法的实现,进而导致类可以实现多个接口而不至于担心不同接口出现相同方法签名却有不同实现的冲突(1.8之前)。
-
泛型工具类
-
StringBuffer/StringBuilder
String | StringBuffer | StringBuilder |
---|---|---|
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间 | StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量 | 可变类,速度更快 |
不可变 | 可变 | 可变 |
* | 线程安全 | 线程不安全 |
* | 多线程操作字符串 | 单线程操作字符串 |
-
线程的几个状态说一下?
-
新建(NEW):新创建了一个线程对象。
-
可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
-
运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
-
阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法, JVM会把该线程放入等待队列(waitting queue)中。 (二). 同步阻塞:运行(running)的线程在获取对象的同步锁时, 若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。 (三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法, 或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、 join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
-
死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
-
-
进程和线程的区别:
通信方式之间的差异
因为那个根本原因,实际上只有进程间需要通信,同一进程的线程共享地址空间,没有通信的必要,但要做好同步/互斥,保护共享的全局变量。
而进程间通信无论是信号,管道pipe还是共享内存都是由操作系统保证的,是系统调用.
-
一、进程间的通信方式
- 管道( pipe ):
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。 - 有名管道 (namedpipe) :
有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。 - 信号量(semophore ) :
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 - 消息队列( messagequeue ) :
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 - 信号 (sinal ) :
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存(shared memory ) :
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。 - 套接字(socket ) :
套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。
- 管道( pipe ):
-
二、线程间的通信方式
- 锁机制:包括互斥锁、条件变量、读写锁
互斥锁提供了以排他方式防止数据结构被并发修改的方法。
读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。 - 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
- 信号机制(Signal):类似进程间的信号处理
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
- 锁机制:包括互斥锁、条件变量、读写锁
-
创建线程的方式有几种?
-
继承Thread
public class ThreadDemo extends Thread { @Override public void run() { System.out.println("继承Thread方法!"); } public static void main(String[] args) { new ThreadDemo().start(); } }
-
实现Runnable接口
public class RunnableDemo implements Runnable { public static void main(String[] args) { Thread thread = new Thread(new RunnableDemo()); thread.start(); } @Override public void run() { System.out.println("实现Runnable方式!"); } }
-
实现Callable接口
class SomeRunnable implements Callable { public void call() { //do something here } } Runnable oneRunnable = new SomeRunnable(); //创建一个类对象 Thread oneThread = new Thread(oneRunnable); //由Runnable创建一个Thread对象 oneTread.start(); //启动线程
-
使用Executor框架来创建线程池。
public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
1、corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
2、maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
3、keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
4、workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。
5、threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
6、handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略
-
-
Callable 与Runnable区别总结:
Callable定义的方法是call,而Runnable定义的方法是run。
Callable的call方法可以有返回值,而Runnable的run方法不能有返回值,这是核心区别。
Callable的call方法可抛出异常,而Runnable的run方法不能抛出异常。 -
创建线程池的几种方式答案
java中创建线程池的方式一般有两种:- 通过Executors工厂方法创建
- 通过new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue)自定义创建
-
异常的种类
-
try/catch 是可以捕获异常和错误的,两者都可以捕获
-
ReentrantLock
ReentrantLock常常对比着synchronized来分析,我们先对比着来看然后再一点一点分析。 (1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。 ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。 (2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁; ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。 (3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。 ReentrantLock好像比synchronized关键字没好太多,我们再去看看synchronized所没有的, 一个最主要的就是ReentrantLock还可以实现公平锁机制。什么叫公平锁呢?也就是在锁上等待 时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。
-
公平锁,非公平锁
- i. 公平锁:多个线程按照申请锁的顺序区获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到。优点:所有的线程都能得到资源,不会饿死在队列中。缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
- ii. 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
-
乐观锁、悲观锁,以及其实现类
-
1.乐观锁(版本号机制),在数据表中加上一个版本号version字段,以用来表示数据被修改的次数,当数据被修改时,version值会加一,当线程A要更新数据值时,在读取数据的同时也会读取version值,再提交更新时,若刚才读的version值和当前数据库中的version值相等才更新,否则重试更新操作,直到更新成功。
-
2.乐观锁(CAS机制:compare and swap):内存地址V、旧的预期值A、要修改的新值B,要修改变量时,只有当变量的预期值A与内存地址V中的实际值相同时,才会将V中对应的值改为B。
-
3.CAS的问题:CPU的开销很大,再并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给cpu带来很大的压力;不能保证代码块的原子性,CAS保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性,比如需要同时保证3个变量共同进行原子性的更新,就不得不用Synchronized了
-
4.悲观锁(Synchronized):Synchronized用法:方法声明时使用(即一次只能有一个线程进入该方法)、代码块(①synchronized(this),类方法中的对象锁,一个线程访问一个对象的synchronized块时,别的线程可以访问非synchronized块;②main方法中的对象锁,其他试图访问被锁上的对象的线程阻塞,等待锁住该对象的线程释放后,才能竞争这个对象)、修饰静态方法(静态方法类似类)、修饰类class ClassName {public void method() {synchronized(ClassName.class)(类似静态方法,所有一个对象的对象公用一把锁) 。
-
-
AQS的底层实现?
AQS原理
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
注意:AQS是自旋锁:在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功
实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物。AQS维护了一个volatile int state和一个FIFO线程等待队列,多线程争用资源被阻塞的时候就会进入这个队列。state就是共享资源,其访问方式有如下三种:
getState();setState();compareAndSetState();
AQS 定义了两种资源共享方式:
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
- ReentrantLock怎么用AQS实现公平锁,非公平锁?
- GcRoot的种类:
1.虚拟机栈:栈帧中的本地变量表引用的对象
2.native方法引用的对象
3.方法区中的静态变量和常量引用的对象
设计模式
大牛的23种设计模式及代码实现全解析
设计模式的六大原则:
一、单一职责原则(Single Responsibility Principle)
定义:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
二.开闭原则(Open-Closed Principle, OCP)
定义:一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展
三、里氏代换原则(Liskov Substitution Principle, LSP)
定义:里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。
四、依赖倒置原则(Dependence Inversion Principle,DIP)
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象,其核心思想是:要面向接口编程,不要面向实现编程。
五、接口隔离原则(Interface Segregation Principle, ISP)
使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
六、迪米特法则(Law of Demeter, LoD)
定义:迪米特法则(Law of Demeter, LoD):一个软件实体应当尽可能少地与其他实体发生相互作用。
设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
- 手写一个单例模式
- 懒汉式
public class LazySingleton {
private static volatile LazySingleton instance = null;
private LazySingleton (){
}
public static LazySingleton getInstance(){
if (instance != null){
instance = new LazySingleton ();
}
return instance;
}
}
双重检测机制的懒汉式单例模式
public class Singleton {
private volatile Singleton instance = null;
//私有构造函数
private Singleton(){};
//双重检测机制
public Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class){
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 饿汉式
public class HungrySingleton{
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return instance;
}
}
- 代理模式
- 工厂模式
JVM
-
jvm,jvm的垃圾回收机制
① 新建的对象,大部分存储在Eden中
② 当Eden内存不够,就进行Minor GC释放掉不活跃对象;然后将部分活跃对象复制到Survivor中(如Survivor1),同时清空Eden区
③ 当Eden区再次满了,将Survivor1中不能清空的对象存放到另一个Survivor中(如Survivor2),同时将Eden区中的不能清空的对象,复制到Survivor1,同时清空Eden区
④ 重复多次(默认15次):Survivor中没有被清理的对象就会复制到老年区(Old)
⑤ 当Old达到一定比例,则会触发Major GC释放老年代
⑥ 当Old区满了,则触发一个一次完整的垃圾回收(Full GC)
⑦ 如果内存还是不够,JVM会抛出内存不足,发生oom,内存泄漏。 -
JVM类加载的过程以及讲解一下双亲委派机制;
类加载的过程:
双亲委派机制的作用有什么?
JVM启动时就会通过bootstarp类加载器把rt.jar下面的核心类加载进来,所以自己重写的不会被加载
-
JVM介绍一下,从类加载器到常说的五个再到解释器和编译器和本地方法库及接口,大概说了以下,主要说了以下java为什么采用解释器和编译器并行的架构。
答案
解释器的执行,抽象的看是这样的:输入的代码 -> [ 解释器 解释执行 ] -> 执行结果
而要JIT编译然后再执行的话,抽象的看则是:输入的代码 -> [ 编译器 编译 ] -> 编译后的代码 -> [ 执行 ] -> 执行结果。编译器编译成机器码耗时,但是之后执行速度快;
解释器单次解释没有编译后的执行快,解释器执行节约内存;
综上:重复比较多的代码块编译后执行,少量单次运行的代码块解释器运行,比较节约时间,提高代码运行效率。通过编译器增加代码的执行效率,通过解释器增加对环境的兼容和灵活性。
-
GC,会引起重GC的情况 ?垃圾回收器 CMS和G1?过程,增量和原始快照算法,G1要解决的问题以及具体是如何解决的答案
CMS收集器仅作用于老年代的收集,是基于标记-清除算法的,它的运作过程分为4个步骤: 初始标记(CMS initial mark) 并发标记(CMS concurrent mark) 重新标记(CMS remark) 并发清除(CMS concurrent sweep) G1重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。 这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。 区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操 作在多长时间内完成。即G1提供了接近实时的收集特性。G1 的主要关注点在 于达到可控的停顿时间,在这个基础上尽可能提高吞吐量。 G1 使用了停顿预测模型来满足用户指定的停顿时间目标,并基于目标来选择进 行垃圾回收的区块数量。G1 采用增量回收的方式,每次回收一些区块,而不是 整堆回收。要清楚 G1 不是一个实时收集器(只是接近实时),它会尽力满足 我们的停顿时间要求,但也不是绝对的,它基于之前垃圾收集的数据统计,估计 出在用户指定的停顿时间内能收集多少个区块
JavaEE
-
cookie和session有什么区别链接:
1、cookie数据存放在客户的浏览器上,session数据放在服务器上
2、cookie不是很安全,别人可以分析存放在本地的COOKIE并进行
3、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能
4、单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能大于3K。 -
get请求和post请求有什么区别:
1.在浏览器进行回退操作时,get请求是无害的,而post请求则会重新请求一次
2.get请求参数是连接在url后面的,而post请求参数是存放在requestbody内的
3.get请求因为浏览器对url长度有限制(不同浏览器长度限制不一样)对传参数量有限制,而post请求因为参数存放在requestbody内所以参数数量没有限制(事实上get请求也能在requestbody内携带参数,只不过不符合规定,有的浏览器能够获取到数据,而有的不能)
4.因为get请求参数暴露在url上,所以安全方面post比get更加安全
5.get请求浏览器会主动cache,post并不会,除非主动设置
6.get请求参数会保存在浏览器历史记录内,post请求并不会
7.get请求只能进行url编码,而post请求可以支持多种编码方式
8.get请求产生1个tcp数据包,post请求产生2个tcp数据包
9.浏览器在发送get请求时会将header和data一起发送给服务器,服务器返回200状态码,而在发送post请求时,会先将header发送给服务器,服务器返回100,之后再将data发送给服务器,服务器返回200 OK
计算机网络
-
http和https的区别:
1.https协议需要到CA申请证书,一般免费证书较少,因而需要一定费用。
2.http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl/tls加密传输协议。
3.http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4.http的连接很简单,是无状态的;HTTPS协议是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。 -
https的详细加密流程答案:
由于HTTP的内容在网络上实际是明文传输,并且也没有身份验证之类的安全措施,所以容易遭到挟持与攻击HTTPS是通过SSL(安全套接层)和TLS(安全传输协议)的组合使用,加密TCP载荷即HTTP报文内容,同时通过不对称密钥方式认证身份,保证传输的安全可靠
即:HTTP+加密+认证+完整性保护=HTTPS
-
网络层、运输层协议应用
-
三次握手、四次挥手
Spring
-
spring,为什么要用,有什么好处?
1.方便解耦,便于开发(Spring就是一个大工厂,可以将所有对象的创建和依赖关系维护都交给spring管理)
2.spring支持aop编程(spring提供面向切面编程,可以很方便的实现对程序进行权限拦截和运行监控等功能)
3.声明式事务的支持(通过配置就完成对事务的支持,不需要手动编程)
4.方便程序的测试,spring 对junit4支持,可以通过注解方便的测试spring 程序
5.方便集成各种优秀的框架()
6.降低javaEE API的使用难度(Spring 对javaEE开发中非常难用的一些API 例如JDBC,javaMail,远程调用等,都提供了封装,是这些API应用难度大大降低)
一、基于XML的配置
<bean id="userService" class="cn.lovepi.***.UserService" init-method="init" destory-method="destory"></bean>
二、基于注解的配置
@Component:当对组件的层次难以定位的时候使用这个注解
@Controller:表示控制层的组件
@Service:表示业务逻辑层的组件
@Repository:表示数据访问层的组件
三、基于Java类的配置
1.使用@Configuration注解需要作为配置的类,表示该类将定义Bean的元数据
2.使用@Bean注解相应的方法,该方法名默认就是Bean的名称,该方法返回值就是Bean的对象。
3.AnnotationConfigApplicationContext或子类进行加载基于java类的配置
IOC是spring的两大核心概念之一,IOC给我们提供了一个IOCbean容器,这个容器会帮我们自动去创建对象,不需要我们手动创建,IOC实现创建的通过DI(Dependency Injection 依赖注入),我们可以通过写Java注解代码或者是XML配置方式,把我们想要注入对象所依赖的一些其他的bean,自动的注入进去,他是通过byName或byType类型的方式来帮助我们注入。正是因为有了依赖注入,使得IOC有这非常强大的好处,解耦。
可以举个例子,JdbcTemplate 或者 SqlSessionFactory 这种bean,如果我们要把他注入到容器里面,他是需要依赖一个数据源的,如果我们把JdbcTemplate 或者 Druid 的数据源强耦合在一起,会导致一个问题,当我们想要使用jdbctemplate必须要使用Druid数据源,那么依赖注入能够帮助我们在Jdbc注入的时候,只需要让他依赖一个DataSource接口,不需要去依赖具体的实现,这样的好处就是,将来我们给容器里面注入一个Druid数据源,他就会自动注入到JdbcTemplate如果我们注入一个其他的也是一样的。比如说c3p0也是一样的,这样的话,JdbcTemplate和数据源完全的解耦了,不强依赖与任何一个数据源,在spring启动的时候,就会把所有的bean全部创建好,这样的话,程序在运行的时候就不需要创建bean了,运行速度会更快,还有IOC管理bean的时候默认是单例的,可以节省时间,提高性能,
-
注解原理答案
- 什么是注解
注解也叫元数据,例如我们常见的@Override和@Deprecated,注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解
- 什么是注解
-
常用的注解可以分为三类:
- 一类是Java自带的标准注解,包括@Override(标明重写某个方法)、@Deprecated(标明某个类或方法过时)和@SuppressWarnings(标明要忽略的警告),使用这些注解后编译器就会进行检查。
- 一类为元注解,元注解是用于定义注解的注解,包括@Retention(标明注解被保留的阶段)、@Target(标明注解使用的范围)、@Inherited(标明注解可继承)、@Documented(标明是否生成javadoc文档)
- 一类为自定义注解,可以根据自己的需求定义注解
-
springboot和spring的区别
创建独立的Spring应用。
嵌入式Tomcat、Jetty、 Undertow容器(无需部署war文件)。
提供的starters 简化构建配置
尽可能自动配置spring应用。
提供生产指标,例如指标、健壮检查和外部化配置
完全没有代码生成和XML配置要求 -
spring的事务实现原理?一方面spring的事务基于数据库实现底层的回滚和提交(源码涉及到几个重要类,事务控制器和事务状态啥的),其次通过AOP将事务横切进业务逻辑(aop的源码和动态代理的实现底层),再利用事务传播特性解决方法相互调用时的事务处理问题。
-
maven:作用
MyBatis
Redis详解
- redis的底层实现
- 缓存雪崩,缓存穿透,缓存击穿吗,你的项目是如何防止的?解释
- 为什么快?
MySQL
- 一级缓存和二级缓存
一级缓存:
就是Session级别的缓存。一个Session做了一个查询操作,它会把这个操作的结果放在一级缓存中。
如果短时间内这个session(一定要同一个session)又做了同一个操作,那么hibernate直接从一级缓存中拿,而不会再去连数据库,取数据。
它是内置的事务范围的缓存,不能被卸载。
二级缓存:
就是SessionFactory级别的缓存。顾名思义,就是查询的时候会把查询结果缓存到二级缓存中。
如果同一个sessionFactory创建的某个session执行了相同的操作,hibernate就会从二级缓存中拿结果,而不会再去连接数据库。
这是可选的插件式的缓存,在默认情况下,SessionFactory不会启用这个插件。
-
数据库
数据库就是包含了很多数据的容器,当然这些数据可能存在不同的小容器(表)里面,总之,如果用水来形容数据,那么数据库就是水库。 -
数据源
数据源是连接到数据库的一类路径,它包含了访问数据库的信息(地址、用户名、密码)。数据源就像是排水管道。 -
数据库连接
数据库连接是根据数据源产生的实际连接上数据库的路径,数据库连接就像是管道里面的水管,这些水管都按照管道(数据源)的配置访问数据库。当打开了数据连接的时候,就像是打开了水管一样。 -
数据库连接池
每个数据源可能会配置数据库连接池,就像是排水管道的自动化系统。数据库连接池的作用就是维护数据库连接,减少创建和删除数据库连接的操作,来达到减少数据访问时耗的目的。
-Spring Boot切换默认数据源
Spring Boot使用四个默认数据源,其优先顺序如下,jdbc->Hikari->Dbcp->Dbcp2。如果需要使用这些默认数据源,需要在pom.xml文件添加数据库(jdbc和Hikari都是支持MySQL的,不知道能否支持其他数据库如MongoDB)和数据源依赖,使用数据源首先添加依赖
数据库索引,介绍一些建立索引的策略
- 什么是索引
索引是一种特殊的文件(MySql数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针,直接在索引中查找符合条件的选项,加快数据库的查询速度,而不是一行一行去遍历数据后才选择出符合条件的。如果没有索引,执行查询时MySQL必须从第一个记录开始扫描整个表的所有记录,直至找到符合要求的记录。表里面的记录数量越多,这个操作的代价就越高。如果作为搜索条件的列上已经创建了索引,MySQL无需扫描任何记录即可迅速得到目标记录所在的位置。
索引是一种数据结构。数据库的索引一般采用B+树实现。
采用B+树的原因是:
B+树是一种磁盘友好型的数据结构。
a、树的内部节点只存放的索引,不存放数据
b、叶子节点只存放数据,每一个叶子节点都用指针连接起来。所以可以顺序查找所有的数据。
索引类型
索引分单列索引和组合索引。单列索引,即一个索引只包含单个列,一个表可以有多个单列索引,但这不是组合索引。组合索引,即一个索包含多个列。
MySQL支持的索引包括`INDEX、UNIQUE、PRIMARY KEY、FULLTEXT`类型的索引。
- 1、==普通索引INDEX
这是最基本的索引类型,而且它没有唯一性之类的限制,多行记录可以包含相同值。普通索引可以通过以下几种方式创建:创建索引,例如CREATE INDEX indexName ON mytable(username(length));
(如果是CHAR,VARCHAR类型,length可以小于字段实际长度;如果是BLOB和TEXT类型,必须指定 length,下同。)
修改表,例如ALTER mytable ADD `INDEX` [indexName] ON (username(length))
创建表的时候指定索引,例如CREATE TABLE mytable( ID INT NOT NULL, username VARCHAR(16) NOT NULL, INDEX [indexName] (username(length)) );
- 2、==唯一性索引==
和“普通索引”基本相同,但有一个区别:索引列的所有值都只能出现一次,即必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
唯一性索引可以用以下几种方式创建:
创建索引,例如CREATE UNIQUE INDEX <索引的名字> ON tablename (列的列表);
修改表,例如ALTER TABLE tablename ADD `UNIQUE` [索引的名字] (列的列表);
创建表的时候指定索引,例如CREATE TABLE tablename ( [...], UNIQUE [索引的名字] (列的列表) );
创建了一个 mytable表,来进一步说明下组合索引:
CREATE TABLE mytable( ID INT NOT NULL, username VARCHAR(16) NOT NULL, city VARCHAR(50) NOT NULL, age INT NOT NULL );
为了进一步提供MySQL的效率,就要考虑建立组合索引。如下语句将 name, city, age建到一个索引里:
`组合索引`
` ALTER TABLE mytable ADD INDEX name_city_age (username(10),city,age);`
建表时,usernname长度为 16,这里用 10。这是因为一般情况下名字的长度不会超过10,这样会加速索引查询速度,还会减少索引文件的大小,提高INSERT的更新速度。
如果分别在 usernname,city,age上建立单列索引,让该表有3个单列索引,查询时和上述的组合索引效率也会大不一样,远远低于我们的组合索引。虽然此时有了三个索引,但MySQL只能用到其中的那个它认为似乎是最有效率的单列索引.建立这样的组合索引,其实是相当于分别建立了下面三组组合索引:
usernname,city,age ;usernname,city; usernname。
为什么没有 city,age这样的组合索引呢?这是因为MySQL组合索引“最左前缀”的结果。简单的理解就是只从最左面的开始组合。并不是只要包含这三列的查询都会用到该组合索引,下面的几个SQL就会用到这个组合索引:
```sql
SELECT * FROM mytable WHREE username="admin" AND city="郑州"
SELECT * FROM mytable WHREE username="admin"
```
而下面几个则不会用到:
```sql
SELECT * FROM mytable WHREE age=20 AND city="郑州"
SELECT * FROM mytable WHREE city="郑州"
```
- 3、==主键 PRIMARY KEY==
主键是一种特殊的唯一性索引,不允许有空值,但它必须指定为“`PRIMARY KEY`”。一般是在建表的时候同时创建主键索引,例如CREATE TABLE tablename ( [...], PRIMARY KEY (列的列表) );”。也可以通过修改表的方式加入主键,例如“ALTER TABLE tablename ADD PRIMARY KEY (列的列表); ”。==注意:每个表只能有一个主键==。
- 4、==全文索引FULLTEXT==
全文索引`FULLTEXT`可以在VARCHAR或者TEXT类型的列上创建。它可以通过CREATE TABLE命令创建,也可以通过ALTER TABLE或CREATE INDEX命令创建。对于大规模的数据集,通过ALTER TABLE(或者CREATE INDEX)命令创建全文索引要比把记录插入带有全文索引的空表更快。
-
sql注入
#{ } 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符。
例如,sqlMap 中如下的 sql 语句select * from user where name = #{name};
解析为:
select * from user where name = ?;
一个 #{ } 被解析为一个参数占位符 ? 。
${ } 仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换
例如,sqlMap 中如下的 sqlselect * from user where name = '${name}';
当我们传递的参数为 “ruhua” 时,上述 sql 的解析为:
select * from user where name = "ruhua";
预编译之前的 SQL 语句已经不包含变量 name 了。
综上所得, KaTeX parse error: Expected 'EOF', got '#' at position 29: …在动态 SQL 解析阶段,而 #̲{ }的变量的替换是在 DBM…{ } 在预编译之前已经被变量替换了,这会存在 sql 注入问题。 -
删除表的方式有哪些?
drop table;
delete table;
truncate table;三种方式的区别:
1.Drop table删表,表结构都会被删除。而delete table和truncate table 只删除表中的数据,表结构还在。
2.Delete table会写日志,truncate table不会写日志。
3.Delete table效率低,数据可以恢复;truncate table 效率高,数据不可恢复。
truncate table 不仅是删除表里面的数据,而且还会清空表里面主键的标识。也就是说使用过truncate table 的表在重新写入数据的时候,标识符会从0或1重新开始(看你设置的种子号);delete table就是仅仅能删除数据,不清空标识。
-
分页 链接
-
MySQL了解哪些存储引擎
)
-
数据库的四种隔离机制以及他们能分别解决什么并发事务带来的问题?什么是幻读,什么是不可重复读?
事务的四大特性(ACID)1.原子性
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。2.一致性
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。3.隔离性
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。4.持久性
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。- 两个更新事务同时修改同一条数据时,会发生很严重的情况,会造成更新数据的丢失。
- 一个更新事务在更新一条数据时,另一个读取事务读取了还没有提交的更新数据,这种情况会出现读取到脏数据。
- 一个读取事务在读取一条数据时,另一个更新事务同时修改了这条数据,这样会出现不可重复读。
- 一个读取事务在读取数据时,另一个事务插入了一条数据,这样可能多读出一条数据,出现幻读。
以上的这四种情况,前三种是对同一条数据的并发操作,对程序的结果可能产生致命影响。综合以上四种情况可以大致这样简单的理解:
1.修改时允许修改(丢失数据)
2.修改时允许读取(脏读)
3.读取时允许修改(不可重复读)
4.读取时允许插入(幻读)
- mysql的主从复制详细过程
- mysql的索引结构
- b树和b+树及区别
- b+树为什么能三层能存2000多万个,计算过程。
- b+树的叶子节点之间是单链还是双链,页与页之间,页内部呢。
JavaWeb
数据结构与算法
- LRU如何实现效率会更高:
-
首先HashMap是一定要用的,因为只有HashMap才可以做到[公式]时间内的读写,其他的数据结构几乎都不可行。但是只有HashMap解决不了更新以及淘汰的问题,必须要配合其他数据结构进行。这个数据结构需要能够做到快速地插入和删除,其实我这么一说已经很明显了,只有一个数据结构可以做到,就是链表。
链表有一个问题是我们想要查询链表当中的某一个节点需要O(n)的时间,这也是我们无法接受的。但这个问题并非无法解决,实际上解决也很简单,我们只需要把链表当中的节点作为HashMap中的value进行储存即可,最后得到的系统架构如下:
整体的设计思路是,可以使用 HashMap 存储 key,这样可以做到 save 和 get key的时间都是 O(1),而 HashMap 的 Value 指向双向链表实现的 LRU 的 Node 节点,如图所示。
class LRUCache {
private Map<Integer, Integer> map;
private int cap;
public LRUCache(int capacity) {
map = new LinkedHashMap<>();
cap = capacity;
}
public int get(int key) {
// key不存在,访问不到
if (!map.containsKey(key)) {
return-1;
}
// key存在,需要将keyPair放到最后
int val = map.remove(key);
map.put(key, val);
return val;
}
public void put(int key, int value) {
// key存在,将存在的keyPair放到最后,并更新上新value
if (map.containsKey(key)) {
map.remove(key);
map.put(key, value);
return;
}
// key不存在,直接放到最后
map.put(key, value);
// 检查缓存容量,超出上限,则移除第一个
if (map.size() > cap) {
map.remove(map.entrySet().iterator().next().getKey());
}
}
}
作者:polynomial
链接:https://leetcode-cn.com/problems/lru-cache/solution/java-shi-yong-linkedhashmapshi-xian-lru-t279i/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 字符串转字符数组:
- 排序算法
String str = "12345";
char[] chars = str.toCharArray();
for(Character chr : chars){
System.out.println( (int)(chr - 0x30));
}
Linux命令行链接
- 查看CPU,GPU命令在这里插入代码片
总核数 = 物理CPU个数 X 每颗物理CPU的核数
总逻辑CPU数 = 物理CPU个数 X 每颗物理CPU的核数 X 超线程数
查看物理CPU个数在这里插入代码片
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
查看每个物理CPU中core的个数(即核数)
cat /proc/cpuinfo| grep "cpu cores"| uniq
查看逻辑CPU的个数
cat /proc/cpuinfo| grep "processor"| wc -l
Linux指令
(1)指令
常用命令
a) Find -iname “xxxx” 查找指定文件名文件 / Find ~ -empty 查找home下空文件
Find /usr -type f –size +1024k 查找超过10mb的文件
b) Ls:ls-lh 显示文件大小 ls-ltr一最后修改时间升序 ls-F显示文件类型
c) Pwd:输出当前工作目录
d) Cd:切换工作目录
e) Mkdir ~/temp 在home下创建一个名为temp的目录 mkdir -p xxx/xxx可以创建一个路径上所有不存在的目录
f) Df -h:显示文件系统的磁盘使用情况
g) Uptime:查看系统运行时间和平均负载 /w or top也可以
h) Rm -i xxx 删除文件前先确认 / rm -r example 递归删除文件夹所有文件,并删除该文件夹。
i) Mv -i file1 file2 重命名,file2存在会提示是否 / mv -v file1 file2 重命名过程会输出
j) Cp -p file1 file2 拷贝file1到file2,并保持文件的权限、属主和时间戳
k) Cat file1 file2 先打印file1的内容,在打印file2的内容
l) Tail -n Nfilename.txt 显示文件最后的n行(默认十行)
m) Less xxx.log 再不加载整个文件的前提下显示文件内容Ctrl+f /ctrl+B 向前/向后滚屏
n) More xxx 查看整个文件的所有内容。
通用命令
A) Grep:grep -i “xxx” demo_file 查找文件中某个字符串,输出该行
grep -A 3 -I “xxx” demo_file 输出匹配成功的行以及该行之后的三行
grep -r “xxx“ * 在一个文件夹中递归查询包含指定字符串的文件
B) Sed,将dos系统中的文件转为unix格式文件
C) Vim 打开文件并跳到第十行:vim+10 filename.txt
D) Diff:diff -w name_list.txt name_list_new.txt 比较,忽略空白符
E) Sort xxx.txt 升序对文件内容排序 / -r 降序
压缩相关
Tar(创建解压.tar文件)、gzip(创建解压*.gz文件)、bzip2(创建解压*.bz2)、unzip(解压*.zip文件)
网络相关
A) Ifconfig 查看和配置linux系统的网络接口
B) Ping ping一个远程主机
C) ftp 连接ftp服务器并下载多个文件
D) ssh 登录到远程主机 ssh -v 显示ssh客户端版本
(2)linux体系结构:
① 用户空间:用户的应用程序、C库
② 内核空间:系统调用接口、内核、平台架构相关代码
③ linux内核:控制系统上所有硬件和软件
④ Linux使用的进程间通信方式:管道、信号、消息队列、共享内存、信号量、套接字。
输入输出
import java.util.*;
public class Main {
public static void main(String args[]) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
String[] split = sc.nextLine().split(",");
Arrays.sort(split);
System.out.println(String.join(",", split));
}
}
}
排序算法
稳定与非稳定:
如果一个排序算法能够保留数组中重复元素的相对位置
则可以被称为是 稳定
的。反之,则是 非稳定 的
。
冒泡排序
a、冒泡排序,是通过每一次遍历获取最大/最小值
b、将最大值/最小值放在尾部/头部
c、然后除开最大值/最小值,剩下的数据在进行遍历获取最大/最小值
d、代码实现
public static void main(String[] args) {
int arr[] = {8, 5, 3, 2, 4};
//冒泡
for (int i = 0; i < arr.length; i++) {
//外层循环,遍历次数
for (int j = 0; j < arr.length - i - 1; j++) {
//内层循环,升序(如果前一个值比后一个值大,则交换)
//内层循环一次,获取一个最大值
if (arr[j] > arr[j + 1]) {
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
}
选择排序
a、将第一个值看成最小值
b、然后和后续的比较找出最小值和下标
c、交换本次遍历的起始值和最小值
d、说明:每次遍历的时候,将前面找出的最小值,看成一个有序的列表,后面的看成无序的列表,然后每次遍历无序列表找出最小值。
public static void main(String[] args) {
int arr[] = {6, 5, 3, 2, 4};
//选择
for (int i = 0; i < arr.length; i++) {
//默认第一个是最小的。
int min = arr[i];
//记录最小的下标
int index = i;
//通过与后面的数据进行比较得出,最小值和下标
for (int j = i + 1; j < arr.length; j++) {
if (min > arr[j]) {
min = arr[j];
index = j;
}
}
//然后将最小值与本次循环的,开始值交换
int temp = arr[i];
arr[i] = min;
arr[index] = temp;
//说明:将i前面的数据看成一个排好的队列,i后面的看成一个无序队列。每次只需要找无需的最小值,做替换
}
}
插入排序
a、默认从第二个数据开始比较。
b、如果第二个数据比第一个小,则交换。然后在用第三个数据比较,如果比前面小,则插入(狡猾)。否则,退出循环
c、说明:默认将第一数据看成有序列表,后面无序的列表循环每一个数据,如果比前面的数据小则插入(交换)。否则退出。
public static void main(String[] args) {
int arr[] = {7, 5, 3, 2, 4};
//插入排序
for (int i = 1; i < arr.length; i++) {
//外层循环,从第二个开始比较
for (int j = i; j > 0; j--) {
//内存循环,与前面排好序的数据比较,如果后面的数据小于前面的则交换
if (arr[j] < arr[j - 1]) {
int temp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = temp;
} else {
//如果不小于,说明插入完毕,退出内层循环
break;
}
}
}
}
希尔排序
a、基本上和插入排序一样的道理
b、不一样的地方在于,每次循环的步长,通过减半的方式来实现
c、说明:基本原理和插入排序类似,不一样的地方在于。通过间隔多个数据来进行插入排序。
public static void main(String[] args) {
int arr[] = {7, 5, 3, 2, 4};
//希尔排序(插入排序变种版)
for (int i = arr.length / 2; i > 0; i /= 2) {
//i层循环控制步长
for (int j = i; j < arr.length; j++) {
//j控制无序端的起始位置
for (int k = j; k > 0 && k - i >= 0; k -= i) {
if (arr[k] < arr[k - i]) {
int temp = arr[k - i];
arr[k - i] = arr[k];
arr[k] = temp;
} else {
break;
}
}
}
//j,k为插入排序,不过步长为i
}
}
快速排序
a、确认列表第一个数据为中间值,第一个值看成空缺(低指针空缺)。
b、然后在剩下的队列中,看成有左右两个指针(高低)。
c、开始高指针向左移动,如果遇到小于中间值的数据,则将这个数据赋值到低指针空缺,并且将高指针的数据看成空缺值(高指针空缺)。然后先向右移动一下低指针,并且切换低指针移动。
d、当低指针移动到大于中间值的时候,赋值到高指针空缺的地方。然后先高指针向左移动,并且切换高指针移动。重复c、d操作。
e、直到高指针和低指针相等时退出,并且将中间值赋值给对应指针位置。
f、然后将中间值的左右两边看成行的列表,进行快速排序操作。
public static void main(String[] args) {
int arr[] = {7, 5, 3, 2, 4, 1, 8, 9, 6};
//快速排序
int low = 0;
int high = arr.length - 1;
quickSort(arr, low, high);
}
public static void quickSort(int[] arr, int low, int high) {
//如果指针在同一位置(只有一个数据时),退出
if (high - low < 1) {
return;
}
//标记,从高指针开始,还是低指针(默认高指针)
boolean flag = true;
//记录指针的其实位置
int start = low;
int end = high;
//默认中间值为低指针的第一个值
int midValue = arr[low];
while (true) {
//高指针移动
if (flag) {
//如果列表右方的数据大于中间值,则向左移动
if (arr[high] > midValue) {
high--;
} else if (arr[high] < midValue) {
//如果小于,则覆盖最开始的低指针值,并且移动低指针,标志位改成从低指针开始移动
arr[low] = arr[high];
low++;
flag = false;
}
} else {
//如果低指针数据小于中间值,则低指针向右移动
if (arr[low] < midValue) {
low++;
} else if (arr[low] > midValue) {
//如果低指针的值大于中间值,则覆盖高指针停留时的数据,并向左移动高指针。切换为高指针移动
arr[high] = arr[low];
high--;
flag = true;
}
}
//当两个指针的位置相同时,则找到了中间值的位置,并退出循环
if (low == high) {
arr[low] = midValue;
break;
}
}
//然后出现有,中间值左边的小于中间值。右边的大于中间值。
//然后在对左右两边的列表在进行快速排序
quickSort(arr, start, low -1);
quickSort(arr, low + 1, end);
}
归并排序
a、将列表按照对等的方式进行拆分
b、拆分小最小快的时候,在将最小块按照原来的拆分,进行合并
c、合并的时候,通过左右两块的左边开始比较大小。小的数据放入新的块中
d、说明:简单一点就是先对半拆成最小单位,然后将两半数据合并成一个有序的列表。
public static void main(String[] args) {
int arr[] = {7, 5, 3, 2, 4, 1,6};
//归并排序
int start = 0;
int end = arr.length - 1;
mergeSort(arr, start, end);
}
public static void mergeSort(int[] arr, int start, int end) {
//判断拆分的不为最小单位
if (end - start > 0) {
//再一次拆分,知道拆成一个一个的数据
mergeSort(arr, start, (start + end) / 2);
mergeSort(arr, (start + end) / 2 + 1, end);
//记录开始/结束位置
int left = start;
int right = (start + end) / 2 + 1;
//记录每个小单位的排序结果
int index = 0;
int[] result = new int[end - start + 1];
//如果查分后的两块数据,都还存在
while (left <= (start + end) / 2 && right <= end) {
//比较两块数据的大小,然后赋值,并且移动下标
if (arr[left] <= arr[right]) {
result[index] = arr[left];
left++;
} else {
result[index] = arr[right];
right++;
}
//移动单位记录的下标
index++;
}
//当某一块数据不存在了时
while (left <= (start + end) / 2 || right <= end) {
//直接赋值到记录下标
if (left <= (start + end) / 2) {
result[index] = arr[left];
left++;
} else {
result[index] = arr[right];
right++;
}
index++;
}
//最后将新的数据赋值给原来的列表,并且是对应分块后的下标。
for (int i = start; i <= end; i++) {
arr[i] = result[i - start];
}
}
}
编程题目
反转链表
输入一个链表,反转链表后,输出新链表的表头。
输入:{1,2,3}
返回值:{3,2,1}
public class ListNode{
int val = null;
ListNode next = null;
ListNode(int val){
this.val = val;
}
}
public class Solution {
public ListNode ReverseList(ListNode head) {
ListNode pre = null;
ListNode post = null;
if (head == null){
return null;
}
while (head != null){
post = head.next;
head.next = pre;
pre = head;
head = post;
}
return pre;
}
}