Java语言
八种基本数据类型及其包装类
整型:byte,short,int,long
浮点型:float,double
逻辑型:boolean
字符型:char
原始类型
| 原始类型所占的字节数
| 包装类
|
byte
| 1个字节
| Byte
|
shot
| 2个字节
| Short
|
int
| 4个字节
| Integer
|
long
| 8个字节
| Long
|
float
| 4个字节
| Float
|
double
| 8个字节
| Double
|
boolean
| 1个字节
| Boolean
|
char
| 2个字节
| Character
|
要注意的是基本数据的包装类很多都实现了享元模式。享元模式就是运用共享技术有效地支持大量细粒度对象的复用。用一个常见的面试题来解释
1.判断如下代码的输出,并说出原因
Integer a1 = 40;
Integer a2 = 40;
System.out.println(a1 == a2);
Integer a3 = 200;
Integer a4 = 200;
System.out.println(a3 == a4);
由自动装箱和拆箱可以知道这2种写法是等价的
Integer a1 = 40;
Integer a1 = Integer.valueOf(40);
看一下Integer的valueOf方法
public static Integer valueOf(int i) {
// i的取值范围为[-128,127]
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
IntegerCache是Ingeter的静态内部类,默认创建了[-128,127]的对象,并放到IntegerCache内部的一个cache数组中,在[-128,127]这个范围内的整数对象,不用创建。直接从IntegerCache中的cache数组中根据下标拿就可以,超出这个范围的每次去创建新的对象。其他几种包装类型的常量池和Integer思路都差不多,源码都很相似。
所以答案如下:
Integer a1 = 40;
Integer a2 = 40;
// true
System.out.println(a1 == a2);
Integer a3 = 200;
Integer a4 = 200;
// false
System.out.println(a3 == a4);
包装类缓存的范围如下
包装类
| 缓存范围
|
Byte
| -128~127
|
Short
| -128~127
|
Integer
| -128~127
|
Long
| -128~127
|
Character
| 0~127
|
2.Java一个char类型可以存储中文吗?
可以,因为Java中使用了Unicode字符,不论中文还是因为固定占用2个字节。
char a = '中';
// 中
System.out.println(a);
3.什么是自动装箱,自动拆箱?
自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱
自动装箱时编译器调用valueOf将原始类型值转换成对象,同时自动拆箱时,编译器通过调用类似intValue(),doubleValue()这类的方法将对象转换成原始类型值
// jdk1.5 之前的写法
Integer tempNum1 = Integer.valueOf(5);
int num1 = tempNum1.intValue();
// jdk1.5之后的写法
Integer tempNum2 = 5;
int num2 = tempNum2;
4.为什么要需要基本数据类型的包装类?
(1)Java是面向对象的语言,很多地方需要使用的是对象而不是基本数据类型。例如,List,Map等容器类中基本 数据类型是放不进去的。
(2)包装类在原先的基本数据类型上,新增加了很多方法,如Integer.valueOf(String s)等
5.既然包装类型能完成所有功能,为啥还需要基本类型?
基本数据类型基于数值,对象类型基于引用。基本数据类型存储在栈的局部变量表中。
而对象类型的变量则存储堆中引用,实例放在堆中,因此对象类型的变量需要占用更多的内存空间。
显然,相对于基本类型的变量来说,对象类型的变量需要占用更多的内存空间。
5.写出如下代码的输出
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println(i1 == i2);
System.out.println(i1 == i2 + i3);
System.out.println(i1 == i4);
System.out.println(i4 == i5);
System.out.println(i4 == i5 + i6);
System.out.println(40 == i5 + i6);
输入如下
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
// true
// Integer.valueOf()用了常量池,看上面的源码
System.out.println(i1 == i2);
// true
// + 操作会导致左右2边都转成基本数据类型
// 具体原因看下面
System.out.println(i1 == i2 + i3);
// false
// Integer.valueOf()使用常量池中的对象
// new Integer每次会创建新对象,
System.out.println(i1 == i4);
// false
// 2个不同的对象
System.out.println(i4 == i5);
// true、
// 解释在最下面
System.out.println(i4 == i5 + i6);
// true
// 解释在最下面
System.out.println(40 == i5 + i6);
语句i4 == i5 + i6,因为+这个操作符不适用于Integer对象,首先i5和i6进行自动拆箱操作,进行数值相加,即i4 == 40。然后Integer对象无法与数值进行直接比较,所以i4自动拆箱转为int值40,最终这条语句转为40 == 40进行数值比较
抽象类和接口
接口和抽象类的相似性
- 都不能被实例化
- 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法
接口和抽象类的区别
- 接口只能包含抽象方法,静态方法和默认方法,抽象类则可以包含普通方法
- 接口里只能定义静态常量,不能定义普通成员变量,抽象类里既可以定义普通成员变量,也可以定义静态常量
- 接口不能包含构造器,抽象类可以包含构造器。抽象类的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作
- 接口里不能包含初始化快,抽象类里可以包含初始化块
- 一个类最多只能有一个直接父类,包括抽象类。但一个类可以实现多个接口
StringBuffer和StringBuilder的区别
先来看String类的实现
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
先来说一下final关键字的作用
1.final修饰类时,表明这个类不能被继承
2.final修饰方法,表明方法不能被重写
3.final修饰变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象
可以看到String类和保存变量的value数组都被final修饰,表明String类是不可变的。
StringBuffer和StringBuilder都继承自AbstractStringBuilder类,看一下AbstractStringBuilder类的定义
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
}
看到区别了吗?value数组没有用private和final修饰,说明了StringBuffer和StringBuilder是可变的。
而StringBuilder和StringBuffer的方法是差不多的,只不过StringBuffer在方法上添加了
synchronized关键字,所以在多线程环境下我们要用StringBuffer来保证线程安全,单线程环境下用StringBuilder来获得更高的效率。
看2个类中同一个方法的定义
// StringBuffer
@Override
public synchronized StringBuffer append(char[] str) {
toStringCache = null;
super.append(str);
return this;
}
// StringBuilder
@Override
public StringBuilder append(char[] str) {
super.append(str);
return this;
}
因为StringBuffer和StringBuilder的实现类似,所以性能比较就落在String和StringBuilder之间了。
1.String是不可变对象,每次操作都会生成新的String对象,然后将指针指向新的对象。
2.抽象类AbstractStringBuilder内部提供了一个自动扩容机制,当发现长度不够的时候,会自动进行扩容工作(具体扩容可以看源码,很容易理解),会创建一个新的数组,并将原来数组的数据复制到新数组,不会创建新对象,拼接字符串的效率高。
用源码证实一下
// String
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
// StringBuilder
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
介绍完毕,所以你应该知道这道题应该怎么答了
常见面试题
1.说一下String StringBuffer StringBuilder的区别?
- 都是final类,不允许被继承
- String长度是不可变的,StringBuffer,StringBuilder长度是可变的
- StringBuffer是线程安全的,StringBuilder不是线程安全的。但它们方法实现类似,StringBuffer在方法之上添加了synchronized修饰,保证线程安全
- StringBuilder比StringBuffer拥有更好的性能
- 如果一个String类型的字符串,在编译时可以确定是一个字符串常量,则编译完成之后,字符串会自动拼接成一个常量,此时String的速度比StringBuffer和StringBuilder的性能好的多
- 字符串拼接速度StringBuilder>StringBuffer>String
我用例子解释一下第五条
public static void main(String[] args) {
String a = "a";
String b = "b";
String c = a + b;
String d = "a" + "b" + "c";
}
反编译class文件后是这样的
public static void main(String[] args) {
String a = "a";
String b = "b";
(new StringBuilder()).append(a).append(b).toString();
String d = "abc";
}
看string d,理解了吗?
同时看string c的拼接过程,先生成一个StringBuilder对象,再调用2次append方法,最后再返回一个String对象,知道String比StringBuilder慢的原因了吧
算法和数据结构
二叉树
定义
二叉树是一种树形结构,它的特点是每个节点至多只有两颗子树(即二叉树中不存在度大于2的几点),并且,二叉树的子树有左右之分,其次序不能任意颠倒
性质
- 在二叉树的第i层上至多有个节点(i>=1)
- 深度为k的二叉树至多有个节点(k>=1)
- 对任何一颗二叉树T,如果其终端节点数为,度为2的节点数为,则
- 具有n个节点的完全二叉树的深度为
遍历二叉树
先序遍历:根左右
中序遍历:左根右
后序遍历:左右根
记忆方法:左右的位置不变,先序遍历根在最前面,中序遍历根在中间,同理,后序遍历根就在最后面了
排序
冒泡排序
public class BubbleSort {
//交换元素顺序
public static void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
//冒泡排序
public static void bubbleSort(int[] a) {
for (int i=0; i<a.length - 1; i++) {
for (int j=0; j<a.length - 1 - i; j++) {
if (a[i] > a[i + 1]) {
swap(a, i, i + 1);
}
}
}
}
public static void main(String[] args) {
int[] a = {1, 5, 2, 4, 7, 6};
bubbleSort(a);
//[1, 2, 4, 5, 6, 7]
System.out.println(Arrays.toString(a));
}
}
选择排序
public class SelectSort {
public static void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
public static void selectSort(int[] a) {
for (int i=0; i<a.length; i++) {
int min = a[i];
int index = i;
for (int j=i; j<a.length; j++) {
if (a[j] < min) {
min = a[j];
index = j;
}
}
if (index != i) {
swap(a, i, index);
}
}
}
public static void main(String[] args) {
int[] a = {1, 5, 2, 4, 7, 6};
selectSort(a);
//[1, 2, 4, 5, 6, 7]
System.out.println(Arrays.toString(a));
}
}
快速排序
- 从数列中取出一个数作为基准数
- 分区过程,将比它大的数全放到它的右边,小于或等于它的数全放到它的左边
- 再对左右区间重复第二步,直到各区间只有一个数
public class QuickSort {
public static int sort(int[] a, int low, int high) {
int key = a[low];
while (low < high) {
//从high所指位置向前搜索找到第一个关键字小于key的记录和key互相交换
while (low < high && a[high] >= key) {
high--;
}
a[low] = a[high];
//从low所指位置向后搜索,找到第一个关键字大于key的记录和key互相交换
while (low < high && a[low] <= key) {
low++;
}
a[high] = a[low];
}
//此时low和key相等
a[low] = key;
return low;
}
public static void quickSort(int[] a, int low, int high) {
if (low < high) {
int key = sort(a, low, high);
quickSort(a, low, key - 1);
quickSort(a, key + 1, high);
}
}
public static void main(String[] args) {
int[] a = {1, 5, 2, 4, 7, 6};
quickSort(a, 0, a.length - 1);
//[1, 2, 4, 5, 6, 7]
System.out.println(Arrays.toString(a));
}
}
归并排序
假设初始序列含有n个记录,则可看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到$ \lceil \frac{x}{2} \rceil $个长度为2或1的有序子序列,在两两归并,…,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2-路归并排序
public class MergeSort {
public static void sort(int[] src) {
int[] temp = new int[src.length];
msort(src,temp,0,src.length-1);
}
public static void msort(int[] src,int[] dest,int left,int right) {
if (left < right) {
int mid = (left + right) / 2;
msort(src,dest,0,mid);
msort(src,dest,mid+1,right);
merge(src,dest,0,mid,right);
}
}
public static void merge(int[] src,int[] dest,int left,int mid,int right) {
int i = left;//左边数组的游标
int j = mid + 1;//右边数组的游标
int index = 0;//dest起一个中途存储的作用,这个是dest数组的游标
while (i <= mid && j <= right) {
if (src[i] <= src[j]) {
dest[index++] = src[i++];
} else {
dest[index++] = src[j++];
}
}
//复制左边剩余的数组
while (i <= mid) {
dest[index++] = src[i++];
}
//复制右边剩余的数组
while (j <= right) {
dest[index++] = src[j++];
}
index = 0;
while (left <= right) {
src[left++] = dest[index++];
}
}
public static void main(String[] args) {
int[] arr = {7,5,3,4,2,1,6,2,9,8};
sort(arr);
//[1, 2, 2, 3, 4, 5, 6, 7, 8, 9]
System.out.println(Arrays.toString(arr));
}
}
二分查找
public class Search {
public static void main(String[] args) {
int test1[] = {3,4,5,1,2,7,9};
//false
System.out.println(binarySearch(test1, 10));
//true
System.out.println(binarySearch(test1, 9));
}
//从array数组中查找target的值,找到返回true,否则返回false
public static boolean binarySearch(int[] array, int target) {
//进行二分查找先进行排序
Arrays.sort(array);
int left = 0;
int right = array.length -1;
//注意是小于等于,如从123456中查找6,没等于不行
while (left <= right) {
int mid = (left + right) >> 1;
if (target == array[mid]) {
return true;
} else if (target > array[mid]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return false;
}
}
深度优先搜索
给定图G的初始状态是所有顶点均未曾访问过,在G中任选一顶点v为初始出发点(源点或根节点),则深度优先遍历可定义如下:首先访问出发点v,并将其标记为已访问过,然后从v出发搜索v的每个未曾访问的的邻节点w,并以w为新的出发点继续进行深度优先遍历,直至图中所有和源点v有路径相通的顶点均被访问为止。若此时图中仍有未访问的顶点,则另选一个尚未访问的顶点作为新的源点重复上述过程,直至图中所有顶点均已被访问为止
##广度优先搜索
设图G的初始状态是所有顶点均未访问过。以G中任选一顶点v为起点,则广度优先搜索定义为:首先访问出发点v,接着依次访问v的所有邻接点,,,…,,然后再依次访问与,,,…,邻接的所有未曾访问过的顶点。以此类推,直至图中所有和起点v有路径相通的顶点都已访问到为止。此时从v开始的搜索过程结束。
若G是连通图,则一次就能搜索完所有节点;否则,在图G中另选一个尚未访问的顶点作为新源点继续上述搜索过程,直至G中所有顶点均已被访问为止
用户点击页面到收到结果中间发生了什么,从servlet的角度
- 用户点击页面发送查询请求->Web服务器应用(如Apache)->Web容器应用(如tomcat)
- 容器创建两个对象HttpServletRequest和HttpServletResponse
- 根据URL找到servlet,并为请求创建或分配一个线程,将请求和响应对象传递给这个servlet线程
- 容器调用Servlet的service()方法,根据请求的不同类型,service()方法会调用doGet()和doPost()方法,假如请求是HTTP POST请求
- doPost()查询数据库获得数据,并把数据增加到请求对象
- servlet把请求转发给jsp,jsp为容器生成页面
- 线程结束,容器把响应对象装换为一个HTTP请求,把它发回给客户,然后删除请求和响应对象
MySQL索引优化策略
数据库事务的四个特性
MySQL为什么要用B+树实现
voliate
线程池
HashTable和ConcurrentHashMap的区别
Lock
CountDownLatch
Spring MVC执行流程
单例模式的5种写法
Spring AOP和IOC
spring ioc的用处
nio
netty
jvm
try catch
求出数据库中重复的记录