1、面向对象思想
面向对象的程序核心是由对象组成的,每个对象包含着对用户公开的特殊功能和隐藏的实现部分。
面向对象是基于面向过程而言,两者都是一种思想。
面向过程:强调的是功能行为。(强调过程,动作)
面向对象:将功能封装进对象,强调了具备功能的对象。(强调对象,事物)
面向对象是基于面向过程的,将复杂的事情进行简单化。
2、JAVA面向对象的三大特性
面向对象对象有三大特征:封装,继承,多态。
3、封装思想
将对象不需要让外界访问的成员变量和方法私有化,只提供符合开发者意愿的共有方法来访问这些数据和逻辑,保证了数据的安全和程序的稳定。
具体的实现方法:
使用private修饰符把成员变量进行私有化,防止外部程序直接随意的进行调用和修改成员变量,然后提供public的set和get方法设置和获取成员变量的值。
也可以只在本类使用的方法使用private。
4、JAVA访问权限修饰符
JAVA中主要有:public,private,protected和default(默认访问权限)四种。
public修饰符,具有最大的访问权限,可以访问任何一个在CLASSPATH下的类,接口,异常等。
protected修饰符,主要作用就是保护子类,子类可以访问这些成员变量和方法,其余类不可以访问。
default修饰符,主要是本包的类可以访问。
private修饰符,访问权限仅限于本类内部,在实际开发过程中,大多数成员变量和方法都是使用private修饰。
5、继承
(1)在多个不同的类抽取共有的数据和逻辑,对这些共性的内容进行封装一个新的类即父类,让之前的类来继承这个类,那些共有的内容在子类中就不必重新定义。
(2)JAVA的继承机制是单继承,即一个类只能有一个直接父类。
(3)如果子类和父类中有同名的成员变量和方法,子类可以使用super关键字调用父类的成员变量和方法,上述使用方法前提是成员在子类可见。
(4)在调用子类构造方法时,会隐式的调用父类的构造方法super()。如果父类没有无参数构造方法,为避免编译错误,需要在子类构造方法中显式的调用父类的含参构造方法。
(5)子类创建时调用父类的构造方法:子类需要使用父类的成员变量和方法,所以就要调用父类的构造方法来进行初始化,之后再进行子类成员变量和方法的初始化。因此构造方法是无法被覆盖的。
(6)当子类需要扩展父类的某个方法时,可以覆盖父类方法,但是子类方法访问权限必须大于或者等于父类权限。
(7)继承提高了程序的复用性,扩展性,也是JAVA语言多态特征的前提。
(8)在实际开发,程序设计过程中,并非先有的父类,而是先有了子类中通用的数据和逻辑,然后再抽取封装出来的父类。
6、多态
多态是同一个行为具有多个不同表现形式或形态的能力。
多态就是一个接口,使用不同的实例而执行不同操作。
多态的优点:
(1)消除类型之间的耦合关系
(2)可替代性
(3)可扩充性
(4)接口性
(5)灵活性
(6)简化性
多态存在的三个必要条件:
(1)继承
(2)重写
(3)父类引用指向子类对象:Parent p = new Child();
当使用多态方式调用方法时,首先检查父类是否存在该方法,没有,则编译错误,;如果有,再去调用子类的同名方法。
多态的好处:可以使程序有良好的扩展,并可以对所有的类的对象进行通用处理。
7、实例化过程
(1)JVM读取指定的classpath路径下的src文件,加载到内存,如果有直接父类,也会加载父类。
(2)堆内存分配空间
(3)执行父类,子类静态代码块
(4)对象属性进行默认初始化
(5)调用构造方法
(6)在构造方法中,先调用父类构造方法初始化父类数据
(7)初始化父类数据后,显示初始化,执行子类的构造代码块
(8)再进行子类构造方法的特定初始化
(9)初始化完毕后,将地址赋值给引用
/*
父类
*/
class Parent {
int num = 5;
static {
System.out.println("父类静态代码块");
System.out.println();
}
{
System.out.println("父类构造代码块1," + num);
num = 1;
System.out.println("父类构造代码块2," + num);
doSomething();
System.out.println();
}
Parent() {
System.out.println("父类构造方法1," + num);
num = 2;
System.out.println("父类构造方法2," + num);
doSomething();
System.out.println();
}
void doSomething() {
System.out.println("父类doSomething方法1," + num);
num = 3;
System.out.println("父类doSomething方法2," + num);
System.out.println();
}
}
/*
子类
*/
class Child extends Parent {
int num = 10;
/*
静态代码块,在类加载时执行
*/
static {
System.out.println("子类静态代码块");
System.out.println();
}
/*
构造代码块
*/
{
System.out.println("子类构造代码块1," + num);
num = 11;
System.out.println("子类构造代码块2," + num);
doSomething();
System.out.println();
}
Child() {
System.out.println("子类构造方法1," + num);
num = 12;
System.out.println("子类构造方法2," + num);
doSomething();
System.out.println();
}
void doSomething() {
System.out.println("子类doSomething方法1," + num);
num = 13;
System.out.println("子类doSomething方法2," + num);
System.out.println();
}
}
public class A {
public static void main(String[] args) {
Child child = new Child();
child.num = 20;
child.doSomething();
}
}
输出:
父类静态代码块
子类静态代码块
父类构造代码块1,5
父类构造代码块2,1
子类doSomething方法1,0
子类doSomething方法2,13
父类构造方法1,1
父类构造方法2,2
子类doSomething方法1,13
子类doSomething方法2,13
子类构造代码块1,10
子类构造代码块2,11
子类doSomething方法1,11
子类doSomething方法2,13
子类构造方法1,13
子类构造方法2,12
子类doSomething方法1,12
子类doSomething方法2,13
子类doSomething方法1,20
子类doSomething方法2,13
8、JDK,JRE,JVM之间的区别
JDK(Java SE Development Kit),JAVA标准开发包,它提供了编译,运行JAVA程序所需的各种工具和资源,包括JAVA编译器,JAVA运行时环境,以及常用的JAVA类库等。
JRE( Java Runtime Environment) ,JAVA运行环境,用于运行JAVA的字节码文件,JRE中包括了JVM以及JVM工作所需要的类库,普通用户而只需要安装JRE来运行JAVA程序,而程序开发者必须安装JDK来编译,调试程序。
JVM(Java Virtual Machine),JAVA虚拟机,是JRE的一部分,它是整个JAVA实现跨平台的最核心的部分,负责运行字节码文件。
我们写出来的JAVA代码,想要运行,需要先编译成字节码,那就需要编译器,而JDK就包含了编辑器javac,编译之后的字节码,想要运行,就需要一个可以执行字节码的程序,这个程序 就是JVM(JAVA虚拟机),专门用来执行JAVA字节码的。
如果我们要开发Java程序,那就需要JDK,因为要编译Java源文件。
如果我们只想运行已经编译好的Java字节码文件,也就是*.class文件,那就只需要JRE。
JDK中包含了JRE,JRE中包含了JVM。
另外,JVM在执行Java字节码时,需要把字节码解释为机器指令,而不同的操作系统是有可能不一样的,所以就导致了不同操作系统上的JVM是不一样的,所以我们在安装JDK时需要选择操作系统。
另外,JVM是用来执行Java字节码的,所以凡是某个代码编译之后是Java字节码的,那就都能在JVM上面运行。
9、JAVA中栈内存和堆内存
Java把内存划分成两种:一种是堆内存,一种是栈内存。
在函数定义的一些基本变量和对象的引用变量都在函数的栈内存中分配。当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另做他用。
堆内存用来存放有new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收机制来管理。在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中的这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。
具体的说:堆和栈都是Java用来在Ram中存放数据的地方。Java自动管理栈和堆,程序员不能直接的设置堆或栈。
Java的堆是一个运行时数据区,对象从中分配空间,这些对象通过new,newarray等指令建立,它们不需要程序代码来显示的释放。堆是由垃圾回收来负责的,堆的优势是可以动态的分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(int,short,long,byte,double,float,boolean,char)和对象句柄。栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假如我们同时定义:
int a = 3;
int b = 3;
编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将这个3存放进来,然后将a指向3.接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,会将b直接指向3。这样,就出现了a与b同时指向3的情况。这时,如果再令a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。要注意这种数据的共享与两个对象的引用变量指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b,它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。
String是一个特殊的包装类数据。可以用:
String str = new String("abc");
String str = "abc";
上面两种的形式创建,第一种使用new()来新建对象的,它会存放于堆中。每调用一次就会创建一个新的对象。而第二种是先在栈中创建一个对String类的对象引用变量str,然后查找栈中有没有存放"abc",如果没有,则将"abc"存放进栈,并令str指向"abc",如果已经有"abc",则直接令str指向"abc"。
比较上述的数值是否相等的时候,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==。
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
可以看出str1和str2是指向同一个对象的。
String str1 =new String ("abc");
String str2 =new String ("abc");
System.out.println(str1==str2); // false
用new的方式是生成不同的对象。每一次生成一个。因此用第一种方式创建对个"abc"字符串,在内存中其实值存在一个对象而已,这种写法有利于节省内存空间。同时它可以在一定程度上提高程序的运行速度,因为在JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str1 =new String (“abc”);的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。另一方面,要注意,我们在使用诸如String str1 = “abc”;的格式定义时,总是想当然的认为,创建了String类的对象str。担心陷阱,对象可能并没有创建,而可能只是指向一个先前已经创建的对象。只有通过new()方法才能保证每一次都创建了一个新的对象。由于String类的immurable(不可变)性质,当String变量需要经常变换其值时,应当考虑使用StringBuffer类,以提高程序效率。
10、equals和==的区别
==:比较的是两个变量的值是否相等,对于引用型变量表示的两个变量在堆中储存的地址是否相等,即栈中的内容是否相等。
equals:表示的两个变量是否是同一个对象的引用,即堆中的内容是否相同。
Java中的数据类型,可分为两类:
1.基本数据类型,也称为原始数据类型。它们之间的比较,应用双等号==,比较的是它们的值。
2.复合数据类型,它们用==比较的时候,比较的是它们在内存中存放地址,所以,除非是同一个new出来的对象,它们的比较结果是true,否则比较结果是false。
Java当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals的方法,这个方法的初始行为是比较对象的内存地址。但在一些类库当中这个方法被覆盖掉了,如String,Integer,Date在这些类当中equals有其自身的实现,而不再试比较类在堆内存中的存放地址了。对于符合数据类型之间进行equals比较,在没有覆写equals的情况下,它们之间的比较还是基于它们在内存中存放位置的地址值,因为Object的equals方法也是用双等号进行比较的,所以比较的结果和双等号==的结果相同。
11、八大基本数据类型
1.四个整数类型:byte,short,int,long
2.两个浮点型:float,double
3.一种字符类型:char
4.一种布尔型:boolean
12、数组的概念
数组是相同类型的有序集合。相同类型的的若干个数据,按照一定的先后次顺序排列组合而成。其中,每一个数据被称作为一个数组元素。每个数组元素可以通过一个下标来访问它们。
数组的特点:
1.其长度是确定的。数组一旦被创建,它的大小就是不可改变的。
2.其元素必须是相同类型,不允许出现混合类型(Object除外)。
3.数组中得元素可以是任何数据类型,包括基本类型和引用类型。
4.数组中可以存放重复的元素。
Java语言使用new操作符来创建数组
dataType[] arrayRefVar = new dataType[arraySize];
你还可以使用如下的方式创建数组。
dataType[] arrayRefVar = {value0, value1, …, valuek};
实例
public class TestArray {
public static void main(String[] args) {
// 数组大小
int size = 10;
// 定义数组
double[] myList = new double[size];
myList[0] = 5.6;
myList[1] = 4.5;
myList[2] = 3.3;
myList[3] = 13.2;
myList[4] = 4.0;
myList[5] = 34.33;
myList[6] = 34.0;
myList[7] = 45.45;
myList[8] = 99.993;
myList[9] = 11123;
// 计算所有元素的总和
double total = 0;
for (int i = 0; i < size; i++) {
total += myList[i];
}
System.out.println("总和为: " + total);
}
}
输出结果
总和为: 11367.373
For-Each循环
JDK 1.5 引进了一种新的循环类型,被称为 For-Each 循环或者加强型循环,它能在不使用下标的情况下遍历数组。
语法格式如下:
for(type element: array)
{
System.out.println(element);
}
13、集合和数组的区别
1.长度区别:数组固定,一旦创建无法扩容,集合可以动态扩展容量,可以根据需要动态改变大小)。
2.元素内容:数组只能存储同一种类型。集合存放的类型可以不是一种(不加泛型时添加的类型是Object)。
14、常见集合的分类
Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为二叉树,元素排好序
Map 接口键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全-
│—————–├ LinkedHashMap 双向链表和哈希表实现
│—————–└ WeakHashMap
├ ——–TreeMap 红黑树对所有的key进行排序
└———IdentifyHashMap
1、Collection集合的方法
2、List
(1)ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
(2)LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
(3)Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素
3、Set
1.HashSet
底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存储元素类是否重写hashcode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
具体实现唯一行的比较过程:
存储元素首先会使用hashcode()算法函数生成一个int类型hashcode散列值,然后已经所存储的元素的hashcode值比较,如果hashcode值不相等,则所存储的两个对象一定不相等,此时存储当前的新的hashcode值所处的元素对象;
如果hashcode相等,存储元素的对象还是不一定相等,此时会调用equals()方法来判断两个对象的内容是否相等,如果内容相等,那么就是同一个对象,无需存储;
如果比较的内容不相等,那就是不同的对象,就该存储,此时就要采用哈希表的解决地址冲突算法,在当前hashcode值处类似一个新的链表,在同一个hashcode值的后面存储不同的对象,这就保证了元素的唯一性。
2.LinkedHashSet
底层数据结构采用链表和哈希表共通实现,链表保证元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。
3.TreeSet
底层数据结构采用二叉树来实现,元素唯一且已经排好序;唯一性同样需要重写hashCode()和equals()方法,二叉树结构保证了元素的有序性。根据构造方法不同,分为自然排序(无参构造)和比较器排序(有参构造),自然排序要求元素必须实现Compareable接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排列序列,返回0说明两个对象相同,不需要存储;比较器排序需要在TreeSet初始化的时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法。
4、Map
Map中的集合,元素是成对存在的(理解为夫妻)。每个元素由键与值两部分组成,通过键可以找对所对应的值。
Map中最常用的集合为HashMap,LinkedHashMap集合。
HashMap<K,V>:存放数据采用的哈希表结构,元素的存取顺序不能保持一致。由于要保证间的唯一,不重复,需要重写键的hashCode(),equals()方法。
LinkedHashMap<K,V>:HashMap下有个子类LinkedHashMap,储存数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致;通过哈希表结构可以保证键的唯一,不重复,需要重写键的hashCode(),equals()方法。
JDK1.8之前,哈希表底层采用数据+链表实现,即使用链表处理冲突,同一个hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值一次查找的效率较低。
JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阙值8时,将链表转换为红黑树,这样大大减少了查找时间。下图所示:
说明:
1.进行键值对存储时,先通过hashCode()计算出键(K)的哈希值,然后再数组中查询,如果没有则保存。
2.但是如果找到同样的哈希值,那么直接调用equals()方法来判断它们的值是否相同。只有满足以上两种条件才能认定为是相同的数据,因此对于Java中的包装类里面都重写了hashCode()和equals()方法。
Map接口中最常用的方法:
Map遍历:
1.获取Map集合中所有的键,由于键是唯一的,所以返回一个Set集合存储所有的键。
2.遍历键的Set集合,得到每一个键
3.根据键,获取键所对应的值
public class MapDemo {
public static void main(String[] args) {
//创建Map对象
Map<String, String> map = new HashMap<String,String>();
//给map中添加元素
map.put("1", "孙俪");
map.put("2", "范冰冰");
map.put("3", "柳岩");
//获取Map中的所有key
Set<String> keySet = map.keySet();
//遍历存放所有key的Set集合
Iterator<String> it =keySet.iterator(); **
while(it.hasNext()){ //利用了Iterator迭代器**
//得到每一个key
String key = it.next();
//通过key获取对应的value
String value = map.get(key);
System.out.println(key+"="+value);
}
}
}
15、List和Set的区别
List:有序,按对象插入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出所有元素,再逐一遍历,还可以使用get(int index)获取指定下标的元素。
Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用Iterator接口取出所有元素,再逐一遍历各个元素。
16、ArrayList的工作原理
1.在构造ArrayList时,如果没有指定容量,那么内部会构造一个空数组,如果指定了容量,那么就构造出对应容量大小的数据。
2.在添加元素时,会判断数组容量是否足够,如果不够则会扩容,扩容按1.5倍扩容,容量足够后,再把元素添加到数组中
3.在添加元素时,如果指定了下标,先检查下标是否越界,然后再确认数组容量是否足够,不够则扩容,然后再把新元素添加到指定位置,如果该位置后面有元素则后移。
4.再获取指定下标的元素时,先检查下标是否越界,然后从数组中取出对应位置的元素。
17、ArrayList和LinkedList区别
1.它们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的。
2.由于底层数据结构不同,它们所适用的场景也不同,ArrayList更适合随机查找,LinkedList更适合删除和添加。
3.另外ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做队列来使用。
以下情况使用ArrayList:
频繁访问列表中的某一个元素。
只需要在列表末尾进行添加和删除元素操作。
以下情况使用LinkedList:
需要通过循环迭代来访问列表中的某个元素。
需要频繁的在列表开头,中间,末尾等位置进行添加和删除元素操作。
LinkedList:
情况1:假如stringList为空,那么添加进来的node就是first,也是last,这个node的prev和next都为null;
情况2:假如stringList不为空,那么添加进来的node就是last,node的prev指向以前的最后一个元素,node的next为null;同时以前的最后一个元素的next.
18、说一下HashMap的Put方法
1.根据Key通过哈希算法与运算得出数组下标
2.如果数组下标位置元素为空,则将Key和Value封装成Entry对象(JDK1.7中是Entry对象,JDK1.8是Node对象)并放入该位置。
3.如果数组下标位置不为空,分以下情况
3.1如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进行扩容,如果不用就生成Entry对象,并使用头插法添加到当前位置的链表中
3.2如果是JDK1.8,则会判断当前位置的Node类型,看是红黑树Node,还是链表Node
3.2.1如果是红黑树Node,则将Key和Value封装成一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前Key,如果存在则更新Value。
3.2.2如果此位置上的Node对象是链表节点,则将Key和Value封装成一个链表Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中判断是否存在当前Key,如果存在则更新Value,当遍历完链表后,将新链表Node插入到链表中,插入链表后,会看到当前链表的节点个数,如果大于等于8,那么则会将改链表转换成红黑树
3.2.3将Key和Value封装成Node插入到链表或红黑树后,在判断是否需要进行扩容,如果需要就扩容,如果不需要就结束PUT方法。
19、HashMap的扩容机制原理
JDK1.7版本:
1.先生成新数组。
2.遍历老数组中每个位置上的链表上的每个元素。
3.取每个元素的key,并基于新数组的长度,计算出每个元素在新数组中的下标。
4.将元素添加到新数组中去。
5.所有元素转移完了之后,将新数组复制给HashMap对象的table属性。
JDK1.8版本:
1.先生成新数组。
2.遍历老数组中每个位置上的链表和红黑树。
3.如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去。
4.如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置。
a.统计每个下标位置的元素个数。
b.如果该位置下的元素个数超过了6,则生成一个新的红黑树,并将根节点的添加到新数组中的下标位置。
c.如果该位置下的元素个数没有超过6,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置
5.所有元素转移完了之后,则将新数组赋值给HashMap对象的table属性。
20、HashCode()和equals()之间的关系
在Java中,每个对象都可以调用自己的hashcode()方法得到自己的哈希值(hashcode),相当于对象的指纹信息,通常来说世界上没有完全相同的两个指纹,但是在Java中做不到这么绝对,但是我们仍然可以利用hashcode来做一些提前的判断,比如:
1.如果两个对象的hashCode不相同,那么这两个对象肯定不同的两个对象。
2.如果两个对象的hashCode相同,不代表这两个对象一定是同一个对象,也可能是两个对象。
3.如果两个对象相等,那么它们的hashCode就一定相同。
在Java的一些集合类的实现中,在比较两个对象是否相等是,会根据上面的规则,会先调用对象的hashCode()方法得到hashcode进行比较,如果hashCode不相同,就可以认为这两个对象不相同,如果hashCode相同,那么就会进一步调用equals()方法进行比较。而equals()方法,就是用来最终确定两个对象是不是相等的,通常equals方法的实现会比较重,逻辑比较多,而hashCode()主要就是得到一个哈希值,实际上就是一个数字,相对而言比较轻,所以在比较两个对象时,通常都会先根据hashCode比较一下。
所以我们就要注意,如果我们重写了equals()方法,那么就要注意hashCode()方法,一定要保证遵守上述规则。
21、String、StringBuffer、StringBuilder的区别
1.String是不可变的,如果尝试去修改,会新生成一个字符串的对象,StringBuffer和StringBulider是可变的。
2.StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更高。
如何理解线程安全:
当多个线程访问同一个对象时,如果不进行额外的同步控制或者其他的协调凑在哦,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。
为什么说StringBuffer是线程安全:主要是因为StringBuffer很多方法都是synchronized 修饰的。
synchronized :
1.修饰一个代码块,被修饰的代码块称为同步代码块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2.修饰一个方法,被修饰方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3.修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4.修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。也就是说,当两个线程访问同一个对象中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行改代码块。
22、重载和重写的区别
1.重载(Overload):在一个类中,同名的方法如果有不同的参数列表(比如参数类型不同,参数个数不同)则视为重载。
2.重写(Override):从字面上看,重写就是重新写一遍的意思。其实就是在子类中吧父类本身有的方法重新写一遍。子类继承父类的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型都相同(子类中方法的返回值可以是父类中方法返回值的子类)的情况下,对方法体进行修改,这就是重写。但是要注意子类方法的访问权限不能小于父类的。
23、深拷贝和浅拷贝
深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
1.深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同一个对象。
2.浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象。
24、JAVA中的异常体系
1.Java中的所有异常都来自顶级父类Throwable;
2.Throwable下有两个子类Exception和Error;
3.Error表示非常严重的错误,比如java.lang.StackOverFlowError和Java.lang.OutOfMemoryError,通常这些错误出现时,仅仅想靠程序自己是解决不了的,可能是虚拟机,磁盘,操作系统层面出现的问题,所以通常也不建议在代码中去捕获这些Error,因为捕获的意义不大,因为程序可能已经根本运行不了了。
4.Exception表示异常,表示程序出现Exception时,是可以靠程序自己来解决的,比如NullPointerException、IllegalAccessException等,我们可以捕获这些异常来做特殊处理。
5.Exception的子类通常又可以分为RuntimeException和非RuntimeException两类。
6.RuntimeException表示运行时异常,表示这个异常在代码运行中抛出的,这些异常是非检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能的避免这类异常的发生,比如NullPointerException、IndexOutOfBoundsException等。
7.非RuntimeException表示非运行时异常,也就是我们常说的检查异常,是必须进行处理的异常,如果不处理,程序就不能检查异常通过。如IOException,SQLException等以及用户自定义的Exception异常。
异常总结:
1.java.lang.nullpointerexception
异常的解释是“程序遇上了空指针”,简单的说就是未经初始化的对象或者是不存在的对象。这个异常经常出现在创建图片,调用数组这些操作中,比如图片未经初始化,或者图片创建的时候路径错误等等。对数组操作中出现了空指针,即把数组的初始化和数组元素的初始化混淆起来了。数组的初始化是对数组分配所需要的空间,而初始化后的数组,其中的元素并没有实例化,依然是空的,所以还需要对每个元素都进行初始化。
2.java.lang.arithmeticexception
这个异常的解释是“数学运算异常”,比如程序中出现了除以0这样的运算就会出现这样的异常;
3.java.lang.arrayindexoutofboundsexception
异常的解释是“数组下标越界”
4.java.lang.illegalargumentexception
异常的解释是“方法的参数错误”,很多j2me的类库中的方法在一些情况下都会引发这样的错误,比如音量调节方法中的音量参数如果写成负数就会出现这个异常再比如g.setcolor(int red,int green,int blue)这个方法中的三个值,如果有超过255的也会出现这个异常,因此一旦发现了这个异常,首先检查一下方法调用的参数传递是不是出现错误。
5.java.lang.illegalaccessexception
异常的解释是“没有访问权限”,当应用程序要调用一个类,但当前的方法即没有对该类的访问权限便会出现这个异常。对程序中用了package的情况下标要注意这个异常。
25、在Java的异常处理机制中,什么时候应该抛出异常,什么时候捕获异常?
异常相当于一种提示,如果我们抛出异常,就相当于我们告诉上层方法,我抛出了一个异常,我处理不了这异常,交给你来处理,而对对于上层方法来说,它也需要决定自己能不能处理这个异常,是否也需要交给它的上层。、
所有我们在写一个方法时,我们需要考虑的就是,本方法能否合理的处理该异常,如果处理不了就继续向上抛出异常,包括本方法中在调用另外一个方法时,发现出现了异常,如果这个异常应该由自己来处理,那就捕获该异常进行处理。
26、Java中有哪些类加载器
JDK自带有三个类加载器:BootstrapClassLoader,ExtClassLoader,AppClassLoader。
BootstrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%/lib下的jar包和class文件。
ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class文件。
AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。
27、说说类加载器双亲委派模型
JVM中存在三个默认的类加载器:
BootstrapClassLoader
ExtClassLoader
APPClassLoader
AppClassLoader的父类加载是ExtClassLoader,ExtClassLoader的父类加载器是BootstrapClassLoader。
JVM在加载一个类时,会调用AppClassLoader的loadClass方法来加载这个类,不过在这个方法中,会先使用ExtClassLoader的loadClass方法来加载类,同样ExtClassLoader的loadClass方法中会先使用BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果BootstrapClassLoader没有加载到,南无ExtClassLoader就会自己尝试加载该类,如果没有加载到,那么则会由AppClassLoader来加载这个类。
所以,双亲委派指的是,JVM在加载类时,会委派给Ext和Bootstrap进行加载,如果没有加载到才由自己进行加载。
28、JVM中哪些是线程共享区
堆区和方法区是所有线程共享的,栈、本地方法栈、程序计数器是每个线程独有的。
29、你们项目如何排查JVM问题
对于还在正常运行的系统:
1.可以使用jmap来查看JVM中各个区域的使用情况
2.可以通过jstack来查看线程的运行情况,比如哪些线程阻塞,是否出现了死锁。
3.可以通过jstart命令来查看垃圾回收的情况,特别是fillgc,如果发现fullgc比较频繁,那么就得进行调优了。
4.通过各个命令的结果,或者jvisualvm等工具来进行分析。
5.首先,初步猜测频繁发送fullgc的原因,如果频繁发送fullgc但是又一直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效。
6.同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存。
对于已经发生OOM的系统:
1.一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
2.我们可以利用jsisualvm等工具来分析dump文件
3.根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码。
总之,调优不是一蹴而就的,需要分析,推理,实践,总结,再分析,最终定位到具体的问题。
30、一个对象从加载到JVM,再到被GC清除,都经历了什么过程?
1.首先把字节码文件加载到方法区
2.然后再根据类信息在堆区创建对象
3.对象首先会分配在堆区中年轻代的Eden区,经过一次Minor GC后,如果能够存活,就会进入Suvivor区。
在后续的每次Minor GC中,如果对象一直存活,就会在Sunvivor区来回拷贝,每移动一次,年龄加1
4.当年龄超过15后,对象依然存活,对象就会进入老年代
5.如果经过Full GC,被标记为垃圾对象,那么就会被GC线程清除掉
Minor GC:新生代GC,指发生在新生代的垃圾回收动作,所有的Minor GC都会触发全世界的暂停(stop-the-world),停止应用程序的线程,不过这个线程非常短暂。
Major GC/Full GC:老年代GC,指发生在老年代的GC。老年代与新生代不同,老年代对象存活时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之目的就是要减少内存碎片带来的效率损耗。
所有通过new创建的对象的内存都在堆中分配,堆被划分为新生代和老年代,新生代又被进一步划分为Eden和Survivor区,而Survivor由FromSpace和ToSpace组成。
新生代:新创建的对象都是用新生代分配内存,Eden空间不足时,触发Minor GC,这时会把存活的对象转移进Survivor区。
老年代:老年代用于存放经过多次Minor GC之后依然存活的对象。
31、JVM有哪些垃圾回收算法?
1.标记清除算法:
a.标记阶段:把垃圾内存标记出来
b.清除阶段:直接将垃圾内存回收
c.这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片
2.复制算法:为了标记清除算法的内存碎片问题,就产生了复制算法。复制算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法没有内存碎片,但是它的问题就是在于浪费空间。而且,它的效率跟存活对象的个数有关。
3.标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除。
32、什么是STW?
STW:Stop-The-World,是在垃圾回收算法执行过程中,需要将JVM内存冻结的一种状态。在STW状态下,JAVA的所有线程都是停止执行的-GC线程除外,native方法可以执行,但是,不能与JVM交互。GC各种算法优化的重点,就是减少STW,同时也是JVM调优的重点。
33、对守护线程的理解
线程分为用户线程和守护线程,用户线程就是普通线程,守护线程就是JVM的后台线程,比如垃圾回收线程就是一个守护线程,守护线程会在其他线程都停止运行之后自动关闭。我们可以通过设置thread.setDaemon(true)来把一个线程设置为守护线程。
34、TreadLocal的底层原理
1.TreadLocal是JAVA中所提供的的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻,任意方法中获取缓存的数据。
2.TreadLocal底层是通过TreadLocalMap来实现的,每个Tread对象(注意不是TreadLocal对象)中都存在一个TreadLocalMap,Map的Key为TreadLocal对象,Map的Value为需要缓存的值。
3.如果在线程池中使用的TreadLocal会造成内存泄漏,因为当TreadLocal对象使用完之后,应该要把设置的Key,Value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向TreadLocalMap,TreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了TreadLocal对象之后,手动调用TreadLocal的remove方法,手动清除Entry对象
4.TreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)
35、并发,并行,串行之间的区别
1.串行:一个任务执行完,才能执行下一个任务
2.并行(Parallelism):两个任务同时执行
3.并发(Concurrency):两个任务同时执行,在底层,两个任务被拆分成了很多份,然后一个一个执行,站在更高的角度看来两个任务是同时在执行的
36、java中有几种方法可以实现线程?用什么关键字修饰同步方法?stop()和suspend()为什么不推荐使用?
Java有三个方式实现线程:
1.继承Tread重写run方法
2.实现Runable接口重写run方法
3.实现Callable接口重写call方法
用syschornized关键字修饰同步方法。
独占:在使用suspend和resume方法时,如果使用不当,极易造成公共的同步对象独占,使得其他线程无法访问公共同步对象。
不同步:在使用suspend与resume方法时也容易因为线程而导致数据不同步的情况。
suspend()用于挂起线程。
resume()用于继续执行已挂起的线程。可以使用这两个办法进行线程的同步。
stop()方法强制停止一个正在运行的线程,无论此时线程是何种状态,在停止线程时需要自行指定线程退出逻辑,否则线程会立即退出,不做任何清理操作,非常不安全,会造成数据不一致问题。
37、sleep()和wait()有什么区别
sleep是thred类的静态方法,wait是Object类的方法。
sleep不释放资源。wait()方法释放资源。sleep可以设置时间,到了规定时间调用notify或notifyall唤醒线程。
而wait()方法一般没有时间限制,只能等待其他线程调用notify或notifyall唤醒线程。
sleep可以用在任何地方。而wait(),notify,notifyall则必须在同步方法或同步代码块中。
sleep必须捕获异常,而wait不用捕获异常。
38、线程同步有几种实现方法?
1.它通过syschorrnized关键字修饰方法,或者代码块。
2.使用重入锁实现线程同步,先通过ReentranLock类实例出一个个锁,在线程中先获得锁,结束再释放锁。
3.通过特殊域变量Volatile,但是不建议用,因为Volatile不能操作原子,所以有可能会乱。
39、启动一个线程是用run()还是start()
start(),因为thred进行start方法时会自动调用run方法。
40、当一个线程进入对象的syschorrnized方法后,其他线程是否可进入此对象的其他方法?
1.其他方法前是否加了syschorrnized关键字,如果没加,则能。
2.如果这个方法内部调用了wait,则可以进入其他syschorrnized方法。
3.如果其他方法都加了syschorrnized关键字,并且内部没有调用wait方法,则不能。
4.如果其他线程是static,它用的同步锁是当前类的字节码,与非静态的方法不能同步,因为非静态方法用的是this。
41、线程的基本概念,线程的基本状态以及状态之间的关系
一个线程是进程的一个顺序执行流。同类的多个线程共享一块内存空间和一组系统资源,线程本身有一个供程序执行是的堆栈。线程在切换时负荷小,因此,线程也被称为轻负荷进程。一个进程中可以包含多个线程。
线程状态:
1.新建
1.用new语句创建的线程对象处于新建状态,此时它和其他java对象一样,仅分配类内存。
2.等待
1.当线程在new之后,并且在调用start方法前,线程处于等待状态。
3.就绪
1.当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态。处于这个状态的线程位于Java虚拟机的可运行池中,等待cpu的使用权。
4.运行状态
1.处于这个状态的线程占用cpu,执行程序代码。在并发运行环境中,如果计算机只有一个cpu,那么任何时刻只会有一个线程处于这个状态,只有处于就绪状态的线程才有机会转到运行状态。
5.阻塞状态
1.阻塞状态是值线程因为某些原因放弃cpu,暂时停止运行。当线程处于阻塞状态时,java虚拟机不会给线程分配cpu,知道线程重新进入就绪状态,它才会有机会获得运行状态。
2.阻塞状态有三种:
1.等待阻塞:运行时的线程执行wait()方法,jvm会把线程放入等待池中。
2.同步阻塞:运行时的线程在获取对象同步锁时,若该同步线程被别的线程占用,则jvm把线程放入锁池中。
3.其他阻塞:运行时的线程执行sleep()方法,或者发出I/O请求时,jvm会把线程设为阻塞状态。当sleep()状态超时,或者I/O处理完毕时,线程重新转入就绪状态。
6.死亡状态
当线程执行完run()方法中的代码,或者遇到了未捕获的异常,就会退出run()方法,此时就进入死亡状态,该线程结束生命周期。
42、简述syschorrnized和java.util.concurrent.locks.lock的异同?
相同点:
Lock能完成syschorrnized所实现的所有功能
不同点:
Lock有比syschorrnized更精确的线程语义和更好的性能。syschorrnized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。因为如果不在finally中释放,当抛出异常时,线程直接死掉,但是没有释放锁,使得其他相关线程无法执行。
Lock还有更强大的功能,例如,它的tryLock方法可以非阻塞方式去拿锁。
tryLock()方法:如果获取到了锁立即返回true,如果别的线程正持有锁,立即返回false。
tryLock(long timeout, TimeUnit timeUnit)方法:如果获取了锁立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false。
43、Java中的volatile变量是什么?
用来确保将变量更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量时共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。也就是说,当一个线程修改了这个变量的值,volatile保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但是普通变量做不到这点,普通变量的值在线程间传递需要通过主内存来完成。
在访问volatile变量时不会执行的加锁操作,因此也就不会使线程阻塞,因此volatile变量是一种比syschorrnized关键字更轻量级的同步机制。
volatile性能:
volatile的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障来保证处理器不发生乱序执行。
44、Java中Runable和Callable有什么不同?
1.意义区别:
Runable接口中的run()方法的返回值是void,它做的事情只是存粹地去执行run()方法中的代码而已。
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
2.使用方法区别:
定义Runable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
创建Runable实现类的实例,并以此实例作为Tread的target来创建Thread对象,该Thread对象才是真正的线程对象。
Future的应用场景:
在并发编程中,我们经常用到非阻塞的模型,通过实现Callback接口,并用Future可以来接收多线程的执行结果。
还表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后做出响应的操作。
FutureTask:
一个可取消的异步计算,这个类提供了Future的基本实现,它实现了启动和取消一个计算,查询这个计算是否已完成,恢复计算结果。计算的结果只能在计算已经完成的情况下恢复。如果计算没有完成,get方法会阻塞,一旦计算完成,这个计算将不会被重启和取消,除非调用runAndReset方法。
45、SQL注入
SQL注入是比较常见的网络攻击方式之一,它不是利用操作系统的bug来实现攻击,而是针对程序员编程时的疏忽,通过SQL语句,实现无账号登录,甚至篡改数据库。
SQL注入攻击的总体思路:
1.寻找到SQL注入的位置
2.判断服务器类型和后台数据库类型
3.针对不同的服务器和数据库特点进行SQL注入攻击
46、如何防止SQL注入
1.代码层防止SQL注入攻击的最佳方案就是SQL预编译。
public List orderList(String studentId){
String sql = “select id,course_id,student_id,status from course where student_id = ?”;
return jdbcTemplate.query(sql,new Object[]{studentId},new BeanPropertyRowMapper(Course.class));
}
这样就把我们传进来的参数就会被当做是一个studentId,所以就不会出现SQL注入了。
2.确定每种数据的类型,比如是数字,数据库则必须使用int类型来存储。
3.规定数据长度,能在一定程度上防止SQL注入。
4.严格限制数据库权限,能最大程度的减少SQL注入的危害。
5.避免直接响应一些SQL异常信息,SQL发生异常后,自定义异常进行响应。
6.过滤参数中含有一些数据库的关键词。
@Component
public class SqlInjectionFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req=(HttpServletRequest)servletRequest;
HttpServletRequest res=(HttpServletRequest)servletResponse;
//获得所有请求参数名
Enumeration params = req.getParameterNames();
String sql = "";
while (params.hasMoreElements()) {
// 得到参数名
String name = params.nextElement().toString();
// 得到参数对应值
String[] value = req.getParameterValues(name);
for (int i = 0; i < value.length; i++) {
sql = sql + value[i];
}
}
if (sqlValidate(sql)) {
throw new IOException("您发送请求中的参数中含有非法字符");
} else {
chain.doFilter(servletRequest,servletResponse);
}
}
/**
* 关键词校验
* @param str
* @return
*/
protected static boolean sqlValidate(String str) {
// 统一转为小写
str = str.toLowerCase();
// 过滤掉的sql关键字,可以手动添加
String badStr = "'|and|exec|execute|insert|select|delete|update|count|drop|*|%|chr|mid|master|truncate|" +
"char|declare|sitename|net user|xp_cmdshell|;|or|-|+|,|like'|and|exec|execute|insert|create|drop|" +
"table|from|grant|use|group_concat|column_name|" +
"information_schema.columns|table_schema|union|where|select|delete|update|order|by|count|*|" +
"chr|mid|master|truncate|char|declare|or|;|-|--|+|,|like|//|/|%|#";
String[] badStrs = badStr.split("\\|");
for (int i = 0; i < badStrs.length; i++) {
if (str.indexOf(badStrs[i]) >= 0) {
return true;
}
}
return false;
}
}
47、JDK1.8新特性
1.Lambda表达式:Lambda表达式本质上是一段匿名内部类,也可以是一段可以传递的代码。
1.Lambda最直观的优点:简洁代码。Lambda表达式的语法格式如下:
(pamameters) -> expression
或
(pamameters) ->{statements}
以下是Lambda表达式的重要特性:
可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回返回值,大括号需要指定表达式返回了一个数值。
Lambda表达式实例:
//1.不需要参数,返回值为 5
() -> 5
//2.接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
//3.接收2个参数(数字),并返回他们的差值
(x,y) -> x - y
//4.接收两个int型整数,返回它们的和
(int x,int y) -> x + y
//5.接收一个String对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.println(s)
2.函数式接口:
函数式接口的提出是为了给Lambda表达式的使用提供更好的支持。
什么是函数式接口?
简单来说就是只定义了一个抽象方法的接口,就是函数式接口。并且还提供了注解@Functionallnterface
常见的四大函数式接口:
1.Consumer 《T》:消费型接口,有参无返回值。
@Test
public void test(){
changeStr("hello",(str) -> System.out.println(str));
}
/**
* Consumer<T> 消费型接口
* @param str
* @param con
*/
public void changeStr(String str, Consumer<String> con){
con.accept(str);
}
2.Supplier 《T》:供给型接口,无参有返回值。
@Test
public void test2(){
String value = getValue(() -> "hello");
System.out.println(value);
}
/**
* Supplier<T> 供给型接口
* @param sup
* @return
*/
public String getValue(Supplier<String> sup){
return sup.get();
}
3.Function 《T》:函数式接口,有参有返回值。
@Test
public void test3(){
Long result = changeNum(100L, (x) -> x + 200L);
System.out.println(result);
}
/**
* Function<T,R> 函数式接口
* @param num
* @param fun
* @return
*/
public Long changeNum(Long num, Function<Long, Long> fun){
return fun.apply(num);
}
4.Predicate《T》:断言型接口,有参有返回值,返回值是boolean类型。
public void test4(){
boolean result = changeBoolean("hello", (str) -> str.length() > 5);
System.out.println(result);
}
/**
* Predicate<T> 断言型接口
* @param str
* @param pre
* @return
*/
public boolean changeBoolean(String str, Predicate<String> pre){
return pre.test(str);
}
总结:函数式接口的提出是为了我们更加方便的使用Lambda表达式,不需要自己再手动创建一个函数式接口。
方法引用:
若Lambda体中的内容有方法已经实现了,那么可以使用“方法引用”,也可以理解为方法引用是Lambda表达式的另外一种表现形式并且其语法比Lambda表达式更加简单。
48、HashSet去重原理
HashSet是Java里一种特殊的集合,它不允许重复元素,也就是说,它可以用来去重。HashSet通过使用hash算法来实现去重。
HashSet可以通过hash值来去重,hash值是一种数字代表,他可以唯一标识一个对象。HashSet会将对象转换成hash值,当HashSet发现又相同的hash值的元素时,它会认为它们是重复的,并将其中一个元素抛弃。
Hashset也可以使用equals()方法去重,当Hashset发现有两个元素的hash值相同时,它会调用equals()方法来比较两个元素是否相等,如果相等,它会抛弃其中一个元素。
49、final在Java中有什么作用
1.关键字final表示最终的,不可变的。
2.关键字final可以修饰变量,方法,还有类
3.final修饰的类无法被继承
4.final修饰的方法无法被覆盖,无法被重写,但是,该方法仍然可以被继承
5.final修饰的变量只能赋值一次
6.final修饰引用
1.如果引用为基本数据类型,则改引用为常量,该值无法修改。
2.如果引用为引用数据类型,比如对象,数组,则该对象数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
3.如果引用时类的成员变量,则必须当场赋值,否则编译会出错
50、Java泛型
Java泛型是J2 SE1.5引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类,接口和方法的创建中,分别称为泛型类,泛型接口,泛型方法。
Java泛型的优点
1.类型安全
通过知道使用泛型定义的变量的类型限制,编译器可以在非常高的层次上验证类型假设。通过变量声明中捕获这一附加的类型信息,泛型允许编译实施这些附加的类型约束。类型错误就可以在编译时被捕获了,而不是在运行时当作ClassCastException展示出来。
//没有泛型的情况
public static void main(String[] args) {
ArrayList list = new ArrayList<>();
list.add("11");
list.add(123);//编译正常
}
//有泛型的情况
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("11");
list.add(123);//编译报错
}
2.消除强制类型转换
泛型的一个附带好处是,消除源代码中的许多强制类型转换。`
//没有泛型的代码段需要强制转换
public static void main(String[] args) {
List list = new ArrayList();
list.add(123);
Integer integer = (Integer) list.get(0);
}
//有泛型的代码段不需要强制转换
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
list.add(1);
int s = list.get(0);
}
- 更高的运行效率
避免不必要的装箱,拆箱操作,提高程序的性能。在非泛型编程中,将简单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程中都是具有很大开销的。引入泛型之后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高。
51、Java反射
Java反射机制是指在程序运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。
private String userName;
public Integer userId;
public Integer user;
private String priFunction(){
return "这是一个私有的方法";
}
public String pubFunction(){
return "这个一个公开的方法";
}
public String pubParFunction(String name){
return "这个一个公开带参数的方法"+ name;
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
//ClassLoader的类加载器对象.loadClass(类型全名称)
Class<?> tClass = ClassLoader.getSystemClassLoader().loadClass("com.demo.reflection.Person");
//创建对象
Object obj = tClass.newInstance();
//获取单个Public属性名
Field userId = tClass.getField("userId");
System.out.println(userId.getName());
//设置属性值,一定要有对象
userId.set(obj,12);
//获取属性值
System.out.println(userId.get(obj));
//输出对象
System.out.println(obj);
//获取所有的Public属性名
Field[] fields = tClass.getFields();
for (Field field : fields) {
System.out.println(field.getName());
}
//获取单个Public方法(不含参数)
Method method = tClass.getMethod("pubFunction");
System.out.println(method.getName());
//调用该方法
method.invoke(obj);
//1.2.4获取单个Public方法(含参数)
/* getMethod(String name, Class<?>... parameterTypes)
name:方法名
parameterTypes:参数类型*/
Method pubParFunction = tClass.getMethod("pubParFunction", String.class);
System.out.println(pubParFunction.getName());
//调用该方法
pubParFunction.invoke(obj,1);
//1.2.5获取所有Public方法(不含参数)
Method[] methods = tClass.getMethods();
for (Method method1 : methods) {
System.out.println(method1.getName());
}
//获取私有属性和方法
Field name = tClass.getDeclaredField("userName");
System.out.println(name.getName());
//设置访问权限
name.setAccessible(true);
//设置属性值
name.set(obj,"tom");
//获取属性值
System.out.println(name.get(obj));
//获取方法
Method method1 = tClass.getDeclaredMethod("priFunction", String.class);
method1.setAccessible(true);
method1.invoke(obj,"abc");
}
52、n++和++n的区别
一、运算顺序不同
n++是先使用n的值然后在自增加1。
++n是先自增加1然后再使用自增后n的值。
二、访问内存顺序不同
n++先访问参数n,然后再自增加1.
++n先自增加1,然后再访问参数n
53、Spring常用注解
1、创建Bean实例对象
@Component
在创建的bean的类上面加上@Component(value = “xxx”) value不填的话默认是当前类名,首字母小写
@Server
@Controller
@Repository
2、声明当前类为配置类
@Configuration
3、声明扫描地址
@ComponentScan(basePackages = {“com.spring”})
4、属性注入
1.AutoWired:根据属性类型进行自动装配。
```java
@Service
public class userService {
@Autowired
private userDao userDao;
public void add(){
System.out.println("service中add方法执行");
userDao.update();
}
}
2.@Qualifier:根据属性名称进行注入。
```java
@Service
public class userService {
@Autowired
@Qualifier(value = "userDaoImpl")
private userDao userDao;
public void add(){
System.out.println("service中add方法执行");
userDao.update();
}
@Repository(value = "userDaoImpl")
public class userDaoImpl implements userDao {
@Override
public void update() {
System.out.println("更新操作执行");
}
}
3.@Resource:可以根据类型注入,也可以根据名称注入
类型注入
@Service
public class userService {
@Resource
private userDao userDao;
public void add(){
System.out.println("service中add方法执行");
userDao.update();
}
}
名称注入
@Service
public class userService {
@Resource(name="userDaoImpl")
private userDao userDao;
public void add(){
System.out.println("service中add方法执行");
userDao.update();
}
@Repository(value = "userDaoImpl")
public class userDaoImpl implements userDao {
@Override
public void update() {
System.out.println("更新操作执行");
}
}
4.@Value:注入普通类型属性
@Value(value="abc")
private String name;
5、切面编程
@Aspect //生成代理对象
@Before//前置通知
@AfterReturning//后置通知(返回通知)
@After//最终通知
@AfterThrowing//异常通知
@Around//环绕通知
@EnableAspectJAutoProxy用于开启注解版的Spring AOP功能,类似使用XML方式的aop:aspectj-autoproxy
切面编程代码实例
1.Dao层
public interface ProxyDao {
void add();
}
2.DaoImpl层
@Component
public class ProxyImpl implements ProxyDao {
@Override
public void add() {
System.out.println("add方法执行");
}
}
3.创建代理对象
//增强的类
@Component
@Aspect //生成代理对象
public class ProxyClass {
//前置通知
@Before(value = "execution(* com.spring.dao.impl.ProxyImpl.add(..))")
public void before(){
System.out.println("前置通知");
}
//后置通知(返回通知)
@AfterReturning(value = "execution(* com.spring.dao.impl.ProxyImpl.add(..))")
public void afterReturning(){
System.out.println("后置通知");
}
//最终通知
@After(value = "execution(* com.spring.dao.impl.ProxyImpl.add(..))")
public void after(){
System.out.println("最终通知");
}
//异常通知
@AfterThrowing(value = "execution(* com.spring.dao.impl.ProxyImpl.add(..))")
public void afterThrowing(){
System.out.println("异常通知");
}
//环绕通知
@Around(value = "execution(* com.spring.dao.impl.ProxyImpl.add(..))")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
System.out.println("方法执行之前通知");
//被增强的方法执行
proceedingJoinPoint.proceed();
System.out.println("方法执行之后通知");
}
}
相同切入点进行提取
//切入点相同,共通切入点提取
@Pointcut(value = "execution(* com.spring.dao.impl.ProxyImpl.add(..))")
public void pointcut(){
}
//前置通知
@Before(value = "pointcut()")
public void before(){
System.out.println("前置通知");
}
6、Sping事务
@EnableTransactionManagement //开启事务
@Transactional(propagation = Propagation.REQUIRED)传播行为
@Transactional(isolation = Isolation.REPEATABLE_READ)事务的隔离级别
7、@ResponseBody
加了这个注解的类会将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据或者是XML。
8、@RestController
@RestController = @Controller + @ResponseBody
9、@RequestMapping(“xxxx”)
它用于映射客户端的访问地址,可以被应用于类和方法上面,客户进行访问时,URL应该为类+方法上面的这个注解里面的内容。
54、什么是分布式事务
在了解分布式事务之前首先说一下什么是事务。
事务可以看作是一整个流程,由它一个一个小部分组成,这个部分要嘛全部成功,要嘛全部失败。
在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫数据库事务。
数据库事务的四大特性:ACID
A(Atomic):原子性,构成事务的所有操作,要嘛都执行完成,要嘛全部不执行。
C(Consistency):一致性,在事务执行前后,数据库的一致性约束没有破坏。
I(Isolation):隔离性,数据库事务一般都是并发的,隔离性是指并发的两个事务的执行互不干扰。
D(Durability):持久性,事务完成之后,该事务对数据的个别更改会持久到数据库,且不会被回滚。
事务有三个读问题:脏读,不可重复性读,虚(幻)读
分布式系统会把一整个应用系统分成可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务。
55、分布式事务产生的情景
跨JVM进程产生分布式事务
微服务之间通过远程调用完成事务操作。
跨数据库实例产生分布式事务
单体系统访问多个数据库实例当单体系统需要访问多个数据库时就会产生分布式事务。由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务。
多服务访问同一个数据库实例
不同的微服务访问同一个数据也会产生分布式事务,两个微服务之间有了不同的数据库链接进行数据库操作,此时产生分布式事务。
56、如何解决脏读、幻读、不可重复读
脏读:一个未提交的事务读取到另一个未提交事务的数据。
方法1:事务隔离级别设置为:read committed。
@Transactional(isolation = Isolation.READ COMMITTED)
方法2:读取的时候加排他锁(select …for update),事务提交才会释放锁,修改时加共享锁(update …lock in share mode)。加排他锁之后不能对该条数据再加锁,能查询但不能更改数据。
不可重复读:一个未提交事务读取到另一个已经提交事务的修改数据
方法1:事务隔离级别设置为Repeatable read。
方法2:读取数据时加共享锁,写数据时加排他锁,都是事务提交才释放锁。读取时候不允许其他事物修改该数据,不管数据在事务过程中读取多少次,数据都是一致的,避免了不可重复读问题。
虚读:一个未提交事务读取到另一个已经提交事务的添加数据
方法1:事务隔离级别设置为serializable
57、Final关键字
final在JAVA是一个保留的关键字,可以声名成员变量,方法,类以及本地变量。
一旦使用final关键字来修饰,那么如果想要再次将被final修饰的变量初始化,编译器会报编译错误。
final修饰变量
final修饰成员变量或者时本地变量都叫做final变量。final变量经常和static关键字一起使用,作为常量。final变量是只读的。
public static final String NAME = "xxx";
final修饰方法
final声明方法,final修饰的方法原本就是一个虚方法,现在通过final来声明当前方法不允许在派生类中进一步被覆盖。
class User{
public final String getName(){
return "user:xxx";
}
}
class Li extends User{
@Override
public final String getName(){
return "Li xxx"; //compilation error: overridden method is final
}
}
final修饰类
使用 final 来修饰的类叫作 final 类,它们不能被继承,Java 中有许多类是 final 的,比如 String, Interger 以及其他包装类。下面是 final 类的实例:
final class User{
}
class Li extends User{ //compilation error: cannot inherit from final class
}
持续更新中…