java的基本数据类型有哪些?
byte:占1字节
short:占2字节
int:占4字节
long:占8字节
float:占4字节
double:占8字节
char:占2字节
boolean:true/false
Java中为什么有包装类型
Java是一个面相对象的编程语言,基本类型并不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
比如,在集合类中,是无法将int、double等类型放进去的
因为集合的容器要求元素是Object类型
为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作
String、StringBuilder、StringBuffer的区别
一、String类
String类是Java中最基本、最常用的字符串类。String对象是不可变的,一旦创建了一个String对象,就无法再修改它的值。当对String对象进行修改时,实际上是创建了一个新的String对象,而原来的String对象则保持不变。这种特性被称为“不可变性”。
由于String对象是不可变的,因此每次对String对象进行修改都会产生一个新的String对象,这会造成很多的开销。比如,下面的代码会创建三个不同的String对象:
String str = "hello";str = str + " world";System.out.println(str);
在上面的代码中,第一行创建了一个String对象,第二行对该对象进行了修改,实际上是创建了一个新的String对象,并将其赋值给str变量。第三行打印了最终的字符串,输出结果为“hello world”。
由于String类是不可变的,因此在需要对字符串进行大量修改的场景下,使用String类会产生很多的开销,降低程序的性能。为了解决这个问题,Java提供了StringBuffer和StringBuilder两个类。
二、StringBuffer类
StringBuffer类和String类一样,也用于处理字符串。与String类不同的是,StringBuffer对象是可变的,可以对其进行修改,而不会创建新的对象。
StringBuffer类中提供了一些方法,用于对字符串进行修改。比如,append()方法用于在字符串末尾添加新的内容;insert()方法用于在指定位置插入新的内容;delete()方法用于删除指定位置的内容等。
下面的代码演示了如何使用StringBuffer类对字符串进行修改:
StringBuffer sb = new StringBuffer("hello");sb.append(" world");System.out.println(sb.toString());
在上面的代码中,第一行创建了一个StringBuffer对象,并初始化其值为“hello”。第二行使用append()方法向字符串末尾添加新的内容。第三行打印了最终的字符串,输出结果为“hello world”。
需要注意的是,由于StringBuffer对象是可变的,因此在多线程环境下使用StringBuffer类可能会出现线程安全问题。为了解决这个问题,Java提供了StringBuilder类
三、StringBuilder类
StringBuilder类和StringBuffer类非常类似,也是用于处理可变的字符串。与StringBuffer类不同的是,StringBuilder类的实现不是线程安全的,因此在单线程环境下,StringBuilder类的性能比StringBuffer类更高。
下面的代码演示了如何使用StringBuilder类对字符串进行修改:
StringBuilder sb = new StringBuilder("hello");sb.append(" world");System.out.println(sb.toString());
在上面的代码中,第一行创建了一个StringBuilder对象,并初始化其值为“hello”。第二行使用append()方法向字符串末尾添加新的内容。第三行打印了最终的字符串,输出结果为“hello world”。
需要注意的是,由于StringBuilder对象不是线程安全的,因此在多线程环境下使用StringBuilder类可能会出现线程安全问题。因此,在多线程环境下,应该使用StringBuffer类。
四、区别总结
1. String类是不可变的,每次对String对象进行修改都会创建一个新的String对象,因此在需要对字符串进行大量修改的场景下,使用String类会产生很多的开销。
2. StringBuffer和StringBuilder类是可变的,可以对其进行修改,而不会创建新的对象。
3. StringBuffer类是线程安全的,而StringBuilder类不是线程安全的。
4. 在单线程环境下,StringBuilder类的性能比StringBuffer类更高。
五、结论
在实际开发中,应该根据具体的场景选择适合的字符串处理类。如果需要对字符串进行大量修改,并且在多线程环境下使用,应该使用StringBuffer类。如果在单线程环境下需要对字符串进行大量修改,应该使用StringBuilder类。如果字符串不需要修改,应该使用String类。
如何理解面向对向和面向过程
1. 什么是面向过程
面向过程(Procedure Oriented)是一种以过程为中心的编程思想,是一种自顶而下的编程模式。最典型的面向过程的编程语言就是C语言。
简单地说,在面向过程的开发范式中,程序员需要把问题分解成一个个步骤,每个步骤用函数实现,依次调用即可。
也就是说,在进行面向过程编程时,可以先定义一个函数,然后使用诸如if-else、for-each等方式执行代码。最典型的用法就是实现一个简单的算法,比如实现冒泡排序。
基于面向过程进行的软件开发,其代码都是流程化的,可以明确地看出第一步做什么、第二步做什么。这种方式的代码执行起来效率很高。
但是,面向过程同时存在代码重用性低、扩展能力差、后期维护难度比较大等问题。
2. 什么是面向对象
面向对象(Object Oriented)的雏形最早出现在1960年的Simula语言中。当时的程序设计领域正面临一种危机:在软硬件环境逐渐复杂的情况下,软件如何得到良好的维护?
面向对象程序设计在某种程度上通过强调可重复性解决了这一问题。目前,较为流行的面向对象语言主要有Java、C#、C++、Python、Ruby和PHP等。
简单地说,在面向对象的开发范式中,程序员将问题分解成一个个步骤,对每个步骤进行相应的抽象,形成对象,通过不同对象之间的调用,组合解决问题。
也就是说,在使用面向对象进行编程时,要把属性、行为等封装成对象,然后基于这些对象及对象的能力实现业务逻辑。比如,想要造一辆车,首先要定义车的各种属性,然后将各种属性封装在一起,抽象成一个Car类。
面向对象的编程方法之所以受欢迎,是因为它更加符合人类的思维方式。用这种方式编写出来的代码的扩展性、可维护性都很高。
其实,面向对象也是一种对现实世界的理解和抽象的方法。通过对现实世界的理解和抽象,运用封装、继承、多态等方法,通过抽象出对象的方式进行软件开发。
这里提到的封装、继承、多态便是面向对象的三大基本特征
重载和重写的认识
重载
重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。
每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
最常用的地方就是构造器的重载,比如在ThreadPoolExecutor线程池的实现类中,可看到如下的重载方法。
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
}
方法重载的好处就是让类以统一的方式处理不同类型的一种手段,调用方法时通过传递给他们的不同个数和类型的参数来决定具体使用哪个方法,这就是多态性。
它的特点是:重载发生在本类,方法名相同,参数列表不同,与返回值无关,只和方法名,参数的类型相关。
方法重载时,方法之间需要存在一定的联系,因为这样可以提高程序的可读性,并且我们一般只重载功能相似的方法。
重载规则
被重载的方法必须改变参数列表(参数个数或类型不一样);
被重载的方法可以改变返回类型;
被重载的方法可以改变访问修饰符;
被重载的方法可以声明新的或更广的检查异常;
方法能够在同一个类中或者在一个子类中被重载。
无法以返回值类型作为重载函数的区分标准。
方法的重写规则
返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类
访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。
父类的成员方法只能被它的子类重写。
声明为 final 的方法不能被重写。
声明为 static 的方法不能被重写,但是能够被再次声明。
子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
构造方法不能被重写。
如果不能继承一个类,则不能重写该类的方法。
基于接口实现的重写Thread,是一个普通的线程类,它实现了Runnable接口,并且重写了Runnable这个接口的run方法,这里这么设计的目的是: 避免Java中一个类只能实现一个接口这一规则导致,如果一个类已经继承了其他的接口,但是又想要去实现线程时的问题。
publicclass Thread implements Runnable {
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
由于接口只是用来做规范设计,用来描述某个对象具有什么行为,但是它并没有具体的实现,因此如果需要声明一个线程,就需要实现该接口并且重写里面的抽象方法(接口中未实现的方法都是抽象的,子类必须要重写)。
Thread类中重写了Runnable中的run方法,该方法调用了target.run()。这个target是真正的线程业务实现,Thread只是一个委派设计模式。
因此,如果我们想通过继承Thread来实现线程,则需要按照如下代码的写法来实现,其中target就是代表着子类的App这个对象实例。
public class App extends Thread{
@Override
public void run() {
//doSomething
}
}
由于接口只是一种行为规范,本身不提供实现,因此实现接口的子类,都“必须”要重写父类的方法,这个和类继承是有区别的。
面试题: 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分
区别:
方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
重载方法是否能够根据返回类型进行区分
重载方法无法根据类型来区分, 它只能通过参数类型、参数个数来区分,但是对于重载的方法,是允许修改返回值类型、异常类型、访问等级,但是不能只根据这些类型类做重载。
为什们不能仅根据返回类型来区分重载呢?
原因是,在调用目标方法时,是无法指定返回值类型信息的,这个时候编译器并不知道你要调用哪个函数。
比如在下面这段代码中,当调用max(1,2);时无法确定调用的是哪个,单从这一点上来说,仅返回值类型不同的重载是不应该允许的。
float max(int a, int b);
int max(int a, int b);
可能有同学会问,如果让编译器能够根据上下文语境来判断呢?比如像下面这段代码。
float x=max(1,2);int y=max(2,3);
在实际开发中,很多时候会存在这样一种方法调用max(1,2),并不会去声明返回值,由于这种情况的存在,所以这个理论也不能实现。
函数的返回值只是作为函数运行之后的一个“状态”他是保持方法的调用者与被调用者进行通信的关键。并不能作为某个方法的“标识”
面向对象的三大基本特征是什么?如何理解?
面向对象的三个基本特征是:封装、继承、多态。
封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法。
继承是为了重用父类代码。
多态我们可以总结如下:
指向子类的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,而对于子类中存在而父类中不存在的方法,该引用是不能使用的,尽管是重载该方法。若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)。
多态实现条件:继承、重写、向上转型
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。
为什么不能用浮点数表示金额?
在Java中,不建议使用浮点数(如float和double)来表示金额,主要原因是浮点数不能精确地表示十进制小数,可能导致精度损失和计算错误。这是因为浮点数是基于二进制系统表示的,有些十进制小数在二进制表示下是无限循环的,无法精确表示。由于计算机内部的二进制表示方式和十进制表示方式存在差异,使用Double类型进行浮点数计算时可能会出现多出小数的情况
例如,0.1在二进制中表示为0.00011001100110011...(无限循环)。当我们使用浮点数表示这样的小数时,只能截断为有限位数,这会导致精度损失。当进行加法、减法、乘法和除法等运算时,这种精度损失可能会累积,导致结果不正确。
为了避免这种情况,建议使用java.math.BigDecimal类来表示和计算金额。BigDecimal提供了用于高精度算术运算的方法,能够精确地表示十进制小数,避免浮点数表示和计算中的精度损失问题。虽然使用BigDecimal可能导致性能略有降低,但对于表示金额等需要高精度计算的场景,它是更合适的选择。
什么是反射?为什么需要反射?
什么是Java反射?
Java反射指的是在Java程序运行状态中,对于任何一个类,都可以获得这个类的所有属性和方法;对于给定的一个对象,都能够调用它的任意一个属性和方法。这种动态获取类的内容以及动态调用对象的方法称为反射机制。
反射中的用法有非常非常多,常见的功能有以下这几个:
在运行时获取一个类的 Class 对象
在运行时构造一个类的实例化对象
在运行时获取一个类的所有信息:变量、方法、构造器、注解
反射的优缺点
优点:
增加程序的灵活性:面对需求变更时,可以灵活地实例化不同对象。
缺点:
破坏类的封装性:可以强制访问 private 修饰的信息;
性能损耗:反射相比直接实例化对象、调用方法、访问变量,中间需要非常多的检查步骤和解析步骤,JVM无法对它们优化。
Java中为什么需要反射?
Java中编译类型有两种:
静态编译:在编译时确定类型,绑定对象即通过。
动态编译:运行时确定类型,绑定对象。动态编译最大限度地发挥了Java的灵活性,体现了多态的应用,可以降低类之间的耦合性。
Java反射是Java被视为动态(或准动态)语言的一个关键性质。
反射(reflection)允许静态语言在运行时(runtime)检查、修改程序的结构与行为。
实现Java反射机制的类都位于java.lang.reflect包中:
Class类:代表一个类
Field类:代表类的成员变量(类的属性)
Method类:代表类的方法
Constructor类:代表类的构造方法
Array类:提供了动态创建数组,以及访问数组的元素的静态方法
equals和==的区别是什么
“==”和equals 最大的区别是
“==”是运算符,如果是基本数据类型,则比较存储的值;如果是引用数据类型,则比较所指向对象的地址值。
equals是Object的方法,比较的是所指向的对象的地址值,一般情况下,重写之后比较的是对象的值。
“==”是运算符
①如果比较的对象是基本数据类型,则比较的是其存储的值是否相等;
②如果比较的是引用数据类型,则比较的是所指向对象的地址值是否相等(是否是同一个对象)。
equals()
equals是Object的方法,用来比较两个对象的地址值是否相等。
注意:
equals 方法不能用于比较基本数据类型,如果没有对 equals 方法进行重写,则相当于“==”,比较的是引用类型的变量所指向的对象的地址值
一般情况下,类会重写equals方法用来比较两个对象的内容是否相等。比如String类中的equals()是被重写了,比较的是对象的值。
示例:
public static void main(String[] args) {
//基本数据类型的比较
int num1 = 10;
int num2 = 10;
System.out.println(num1 == num2); //true
//引用数据类型的比较
//String类(重写了equals方法)中==与equals的比较
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); //true,比较地址值:内容相同,因为常量池中只有一个“hello”,所以它们的地址值相同
System.out.println(s1.equals(s2));//true,比较内容:内容相同,因为常量池中只有一个“hello”,所以它们的地址值相同
System.out.println(s1.equals("hello")); //true
String s3 = new String("hello");
String s4 = new String("hello");
System.out.println(s3 == s4); //false,比较地址值:s3和s4在堆内存中的地址值不同
System.out.println(s3.equals(s4)); //true,比较内容:内容相同
//没有重写equals方法的类中==与equals的比较
People p1 = new People();
People p2 = new People();
People p = p2;
System.out.println(p1);//People@135fbaa4
System.out.println(p2);//People@45ee12a7
System.out.println(p); //People@45ee12a7
System.out.println(p1.equals(p2)); //false,p1和p2的地址值不同
System.out.println(p.equals(p2)); //true,p和p2的地址值相同
}
为什么需要克隆?如何实现对象的克隆?深拷贝和浅拷贝的区别?
一、为什么要使用克隆
Java中的克隆是指创建一个与原始对象完全相同的新对象,包括它的属性和值。在Java中,我们可以通过实现Cloneable接口并覆盖Object类中的clone()方法来实现克隆。
使用克隆的主要原因如下:
避免对原始对象的修改。如果需要创建一个新对象,但不想改变原始对象的状态,那么克隆就非常有用了。通过克隆可以创建一个新对象,该对象的属性和原始对象相同,但是对新对象所做的更改不会影响原始对象的状态。
提高性能。在某些情况下,克隆可以比手动创建一个新对象更快。这是因为,当克隆一个对象时,只需要复制现有的数据结构,而不需要重新计算或分配内存。
隐藏实现细节。克隆可以使代码更易于维护,并且可以隐藏一些实现细节。例如,某个对象可能需要使用复杂的初始化过程才能被创建,但是通过克隆,我们可以避免让客户端了解这些实现细节。
需要注意的是,克隆不适用于所有情况。对于某些对象来说,使用克隆可能会导致意外的行为。此外,在某些情况下,克隆可能比手动创建对象更消耗内存。因此,必须仔细考虑是否使用克隆,并评估其优缺点以及潜在的风险。
下面是一个实现浅克隆的示例代码:
用例1:
public class Person implements Cloneable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
Person p1 = new Person("John", 20);
Person p2 = (Person) p1.clone(); // 克隆p1得到p2
p2.setName("Jane");
p2.setAge(22);
System.out.println("p1 name: " + p1.getName() + ", age: " + p1.getAge());
System.out.println("p2 name: " + p2.getName() + ", age: " + p2.getAge());
}
}
在这个示例中,我们定义了一个Person类,并实现了Cloneable接口,重写了clone()方法以支持克隆。在main()方法中,我们创建了一个Person对象p1,并通过调用clone()方法得到了一个新的Person对象p2。然后修改了p2的name和age属性,并输出了两个对象的属性值。
需要注意的是,在进行克隆操作时,被克隆的对象必须实现Cloneable接口并重写clone()方法,且clone()方法必须抛出CloneNotSupportedException异常。而且,浅克隆只能复制对象本身的属性,对于引用类型的属性,只是复制了引用,而没有复制对象本身。如果需要实现深克隆,则需要自行实现复制引用对象的代码.
用例2:
二、如何实现对象克隆
在Java中,实现对象克隆需要做以下几个步骤:
让要被克隆的类实现Cloneable接口。该接口是一个标记接口,用于指示该类可以进行克隆操作。
public class MyClass implements Cloneable {
// class definition here
}
在要被克隆的类中,覆盖Object类的clone()方法。在覆盖该方法时,需要调用super.clone()以获得原始对象的副本,并进行必要的类型转换和处理。
public class MyClass implements Cloneable {
private String name;
private int age;
public MyClass(String name, int age) {
this.name = name;
this.age = age;
}
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
使用clone()方法创建对象的副本。可以使用原始对象的clone()方法来创建其副本。需要注意的是,由于Object类的clone()方法是protected访问级别的,因此只能在该类或其子类中调用该方法。
MyClass original = new MyClass("John", 30);
MyClass cloned = (MyClass) original.clone();
在上面的代码中,我们首先创建了一个MyClass对象original,并将其克隆到变量cloned中。需要注意的是,在使用clone()方法时,需要处理可能抛出的CloneNotSupportedException异常。
需要注意的是,这种方式是浅克隆,即只复制对象的值(包括基本类型和引用类型的地址),而不复制对象所引用的其他对象。如果需要实现深克隆,即同时复制所有相关对象,则需要使用其他方法来实现。
三、深拷贝和浅拷贝区别是什么
Java中的对象赋值操作有两种方式:深拷贝和浅拷贝。
浅拷贝是指在进行对象赋值时,将一个对象的引用赋值给另一个对象,这两个对象共享同一块内存空间。因此,如果修改其中一个对象的属性,另外一个对象也会受到影响。示例如下:
class Person {
public String name;
}
Person person1 = new Person();
person1.name = "Tom";
// 浅拷贝
Person person2 = person1;
person2.name = "Jerry";
System.out.println(person1.name); // 输出"Jerry"
在上述代码中,我们创建了一个Person对象person1,并将其name属性设置为"Tom"。然后我们使用浅拷贝,将person1赋值给person2。接着,我们将person2的name属性修改为"Jerry"。由于person1和person2共享同一块内存空间,因此person1的name属性也变成了"Jerry"。
相反,深拷贝是指在进行对象赋值时,将一个对象完全复制一份,包括其所有的属性和子对象,这两个对象不再共享同一块内存空间。因此,如果修改其中一个对象的属性,另外一个对象不会受到影响。示例如下:
class Person implements Cloneable {
public String name;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
Person person1 = new Person();
person1.name = "Tom";
// 深拷贝
Person person2 = (Person) person1.clone();
person2.name = "Jerry";
System.out.println(person1.name); // 输出"Tom"
在上述代码中,我们创建了一个Person对象person1,并将其name属性设置为"Tom"。然后我们使用深拷贝,通过调用person1的clone()方法来复制一个新的Person对象person2。接着,我们将person2的name属性修改为"Jerry"。由于person1和person2不共享同一块内存空间,因此person1的name属性仍保持为"Tom"。
需要注意的是,要实现深拷贝,必须确保被复制的对象及其所有子对象都实现了Cloneable接口并重写了clone()方法。另外,如果被复制的对象包含类似于文件句柄、网络连接等资源,那么需要在clone()方法中进行特殊处理以避免资源泄漏等问题。
try-catch-finally中如果catch中return了finall还会执行吗?
先说结论,会!
try..catch..finally结构,其执行顺序是先执行try结构体的内容,此时如果try中代码出现异常,那么会执行到catch代码块中进行异常的处理,最后不管有无异常都会执行finally代码块执行一个保底的操作。
具体,我们可以看下面的一块代码:
我们可以看到,代码执行到13行时,1/0会抛出异常,进入catch代码块中,执行完代码块打印函数后,立刻返回。此时,却没有像我们想的一样退出整个代码块,而是执行完finally内代码块的内容才真正的退出。因此,就标题而言,finally只有真正执行完才会退出。
注意,以上只是一般正常的情况。凡事都有例外,碰到以下几种情况,finally也不会被执行:
1.如果代码没有进入try..catch..finally结构就返回,finally不会被执行
2.如果代码没有进入try..catch..finally结构就抛出异常,finally不会被执行
3.如果进入catch时,虚拟机被终止了,finally也不会被执行
String为什么设计成不可变的?
String为什么要设计为不可变对象
1.保证 String 对象的安全性。假设 String 对象是可变的,那么 String 对象将可能被恶意修改。
2.保证 hash 属性值不会频繁变更,确保了唯一性,使得类似 HashMap 容器(实际通过StringTable实现)才能实现相应的 key-value 缓存功能。
3.可以实现字符串常量池。在 Java 中,通常有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,如 String str=“abc”;另一种是字符串变量通过 new 形式的创建,如 String str = new String(“abc”)。
字符串字面量具体是什么?
字面量(literal)是用于表达源代码中一个固定值的表示法(notation),字符串字面量(stringliteral)是指双引号引住的一系列字符,双引号中可以没有字符,可以只有一个字符,也可以有很多个字符。当然这里指的是编译后的class文件中的字符串对象。所以在有些场景下,存在编译器的优化,比如以下代码也会被优化为字符串字面量。
String str1 = "abc" + "def";
除了定义字符串为字面量时,依赖类加载初始化字符串到常量池外,还有其他途径吗?
String类提供了java.lang.String#intern方法,支持我们在运行期间将字符串添加到字符串常量池,以便后面复用,同时该方法返回字符串在常量池的引用。举例如下:
String str1 = "abcdef";
String str2 = new String(str1);
String str3 = str2.intern();
此时,str2对象字符串“abcdef”会因为调用了intern()方法,而被加入到字符串常量池(此时因str1属于字符串字面量因此在类加载时已经加入到常量池,所以不再重复加入),同时str3的引用为常量池中字符串“abcdef”的引用。
Error和Exception的区别和联系?以及常见的RuntimeException。
大白话:
Error(错误)是系统中的错误,程序员是不能改变的和处理的,是在程序编译时出现的错误,只能通过修改程序才能修正。一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。
Exception(异常)表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。
Throwable、Error、Exception、RuntimeException 关系如下类图所示:
Throwable:
Throwable类是java语言中所有错误或者异常的超类。它的两个子类是Error和Exception。
Error:
Error是Throwable的子类,用于指示合理的应用程序不应该试图捕获的严重问题。
一般都是不可控制的(unchecked),用来表示虚拟机内部错误或者底层资源的错误。
Exception:
可以是被控制的(checked)或者不可控制的(unchecked),一般是有认为导致的错误,应该在程序级别处理掉。
Exception类和所有子类(不包括RuntimeException类及其子类)都是检查异常(checked exceptions),它们需要在方法或构造方法的throws子句中声明,可以传递到外部。
RuntimeException:
RuntimeException是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。可能在执行方法期间抛出但未被捕获的RuntimeException的任何子类都无需在 throws子句中进行声明。RuntimeException是Exception的子类,是不可控制的(unchecked)。
常见的几种RuntimeException:
NullPointerException(空指针异常)
IndexOutOfBoundsException(下标越界异常)
IllegalArgumentException - 传递非法参数异常。
ClassCastException - 类型强制转换异常。
NumberFormatException (String转换为指定的数字类型异常)
ArithmeticException -(算术运算异常 如除数为0)
ArrayStoreException - (向数组中存放与声明类型不兼容对象异常)
SecurityException -(安全异常)
UnsupportedOperationException - 不支持的操作异常
下面是一些异常和错误。
java.lang.AbstractMethodError
抽象方法错误。当应用试图调用抽象方法时抛出。
java.lang.AssertionError
断言错。用来指示一个断言失败的情况。
java.lang.ClassCircularityError
类循环依赖错误。在初始化一个类时,若检测到类之间循环依赖则抛出该异常。
java.lang.ClassFormatError
类格式错误。当Java虚拟机试图从一个文件中读取Java类,而检测到该文件的内容不符合类的有效格式时抛出。
java.lang.Error
错误。是所有错误的基类,用于标识严重的程序运行问题。这些问题通常描述一些不应被应用程序捕获的反常情况。
java.lang.ExceptionInInitializerError
初始化程序错误。当执行一个类的静态初始化程序的过程中,发生了异常时抛出。静态初始化程序是指直接包含于类中的static语句段。
java.lang.IllegalAccessError
违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。
java.lang.IncompatibleClassChangeError
不兼容的类变化错误。当正在执行的方法所依赖的类定义发生了不兼容的改变时,抛出该异常。一般在修改了应用中的某些类的声明定义而没有对整个应用重新编译而直接运行的情况下,容易引发该错误。
java.lang.InstantiationError
实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常.
java.lang.InternalError
内部错误。用于指示Java虚拟机发生了内部错误。
java.lang.LinkageError
链接错误。该错误及其所有子类指示某个类依赖于另外一些类,在该类编译之后,被依赖的类改变了其类定义而没有重新编译所有的类,进而引发错误的情况。
java.lang.NoClassDefFoundError
未找到类定义错误。当Java虚拟机或者类装载器试图实例化某个类,而找不到该类的定义时抛出该错误。
java.lang.NoSuchFieldError
域不存在错误。当应用试图访问或者修改某类的某个域,而该类的定义中没有该域的定义时抛出该错误。
java.lang.NoSuchMethodError
方法不存在错误。当应用试图调用某类的某个方法,而该类的定义中没有该方法的定义时抛出该错误。
java.lang.OutOfMemoryError
内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
java.lang.StackOverflowError
堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出时抛出该错误。
java.lang.ThreadDeath
线程结束。当调用Thread类的stop方法时抛出该错误,用于指示线程结束。
java.lang.UnknownError
未知错误。用于指示Java虚拟机发生了未知严重错误的情况。
java.lang.UnsatisfiedLinkError
未满足的链接错误。当Java虚拟机未找到某个类的声明为native方法的本机语言定义时抛出。
java.lang.UnsupportedClassVersionError
不支持的类版本错误。当Java虚拟机试图从读取某个类文件,但是发现该文件的主、次版本号不被当前Java虚拟机支持的时候,抛出该错误。
java.lang.VerifyError
验证错误。当验证器检测到某个类文件中存在内部不兼容或者安全问题时抛出该错误。
java.lang.VirtualMachineError
虚拟机错误。用于指示虚拟机被破坏或者继续执行操作所需的资源不足的情况。
java.lang.ArithmeticException
算术条件异常。譬如:整数除零等。
java.lang.ArrayIndexOutOfBoundsException
数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
java.lang.ArrayStoreException
数组存储异常。当向数组中存放非数组声明类型对象时抛出。
java.lang.ClassCastException
类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。
java.lang.ClassNotFoundException
找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
java.lang.CloneNotSupportedException
不支持克隆异常。当没有实现Cloneable接口或者不支持克隆方法时,调用其clone()方法则抛出该异常。
java.lang.EnumConstantNotPresentException
枚举常量不存在异常。当应用试图通过名称和枚举类型访问一个枚举对象,但该枚举对象并不包含常量时,抛出该异常。
java.lang.Exception
根异常。用以描述应用程序希望捕获的情况。
java.lang.IllegalAccessException
违法的访问异常。当应用试图通过反射方式创建某个类的实例、访问该类属性、调用该类方法,而当时又无法访问类的、属性的、方法的或构造方法的定义时抛出该异常。
java.lang.IllegalMonitorStateException
违法的监控状态异常。当某个线程试图等待一个自己并不拥有的对象(O)的监控器或者通知其他线程等待该对象(O)的监控器时,抛出该异常。
java.lang.IllegalStateException
违法的状态异常。当在Java环境和应用尚未处于某个方法的合法调用状态,而调用了该方法时,抛出该异常。
java.lang.IllegalThreadStateException
违法的线程状态异常。当县城尚未处于某个方法的合法调用状态,而调用了该方法时,抛出异常。
java.lang.IndexOutOfBoundsException
索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。
java.lang.InstantiationException
实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。
java.lang.InterruptedException
被中止异常。当某个线程处于长时间的等待、休眠或其他暂停状态,而此时其他的线程通过Thread的interrupt方法终止该线程时抛出该异常。
java.lang.NegativeArraySizeException
数组大小为负值异常。当使用负数大小值创建数组时抛出该异常。
java.lang.NoSuchFieldException
属性不存在异常。当访问某个类的不存在的属性时抛出该异常。
java.lang.NoSuchMethodException
方法不存在异常。当访问某个类的不存在的方法时抛出该异常。
java.lang.NullPointerException
空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。
java.lang.NumberFormatException
数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。
java.lang.RuntimeException
运行时异常。是所有Java虚拟机正常操作期间可以被抛出的异常的父类。
java.lang.SecurityException
安全异常。由安全管理器抛出,用于指示违反安全情况的异常。
java.lang.StringIndexOutOfBoundsException
字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。
java.lang.TypeNotPresentException
类型不存在异常。当应用试图以某个类型名称的字符串表达方式访问该类型,但是根据给定的名称又找不到该类型是抛出该异常。该异常与ClassNotFoundException的区别在于该异常是unchecked(不被检查)异常,而ClassNotFoundException是checked(被检查)异常。
java.lang.UnsupportedOperationException
不支持的方法异常。指明请求的方法不被支持情况的异常。
final, finally, finalize的区别?
首先,这三个关键字没什么关联,只是放在一起比较像,用法用处完全不同。
final:
final关键字可以用来修饰类、方法、变量(成员变量和局部变量),final用的多,下面详细介绍下
1.1 final 修饰类
final修饰类,表明这个类不能被继承,同时该类内的所有方法都被隐式定义为final。
1.2 final 修饰方法
final 修饰方法,该方法不可以被继承,不可以被重写。因此要注意,如果子类定义相同方法名和参数的函数,不是重写父类方法,是重新定义了一个新方法。
1.3 final 修饰变量
final修饰变量,表示只能被赋值一次,赋值后其值不再改变。
当修饰基本数据类型时,基本数据类型的值初始化后就不会再发生变化。
当修饰引用类型时,初始化后就不能再指向其他对象了,但是对象的值是可以变化的。引用的值本质是一个地址,因此只要地址不变,就不冲突。
final修饰一个成员变量,必须要显式初始化。一种是声明变量时候初始化,一种是声明变量时不初始化,构造函数中对变量初始化。
finally :
finally用于异常处理,只能用在try/catch语句中。
关于finally要注意两点:
1、finally不一定会执行
有两种不会执行:a、在执行try/catch之前异常跳出。b、系统强制退出,例如:System.exit(0);
2、finally中包含return的易错点!!在Java语言的异常处理中,finally块的作用就是为了保证无论出现什么情况,finally块里的代码一定会被执行。
由于程序执行return就意味着结束对当前函数的调用并跳出这个函数体,因此任何语句要执行都只能在return前执行(除非碰到exit函数),因此finally块里的代码也是在return之前执行的。
例如:
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test(){
try {
return 1;
} catch (Exception e) {
return 0;
}finally{
System.out.println("finally");
return 2;
}
}
运行结果:
finally
2
可以知道,finally块中有return语句时,会覆盖函数中的return。
由于java中值传递和引用传递的不同,因此,对基本类型的数据,在finally块中改变return的值对返回值没有任何影响,而对引用类型的数据会有影响
例子:
public static void main(String[] args) {
System.out.println(test());
System.out.println( testStr());
}
private static int test(){
int i = -1;
try {
i= 1;
return i;
} catch (Exception e) {
return i;
}finally{
i=2;
System.out.println("finally");
}
}
private static StringBuilder testStr(){
StringBuilder str = new StringBuilder("hello");
try {
return str;
} catch (Exception e) {
return null;
}finally{
str.append("world");
System.out.println("finally");
}
}
运行结果:
finally
1
finally
helloword
finalize:
finalize()是在java.lang.Object里定义的,也就是说每一个对象都有这么个方法。这个方法在gc启动,该对象被回收的时候被调用。不需要程序员自己手动调用的。只了解,不推荐使用。
int和Integer。
int和Integer的区别
int是基本数据类型,Integer是int的包装类。
Integer必须实例化后才能使用,int变量不需要。
Integer实际是对象的引用,生成一个新对象实际上是生成一个指针指向此对象;而int则是直接存储数据值。
Integer默认值是null,int默认值是0;
Integer的缓存范围是-128到127。
关于Int和Integer比较的一些延伸:
例子:
Integer a= new Integer(100);
Integer b = new Integer(100);
int c = 100;
Integer d = 100;
Integer e = 100;
Integer f = 128;
Integer g =128;
①、System,out.print(a==b);//false
②、System,out.print(a==c);//true
③、System,out.print(a==d);//false
④、System,out.print(d==e);//true
⑤、System,out.print(f==g);//false
解析:
①:Integer变量是对一个Integer对象的引用。通过new生成的Integer变量地址永远不相等。因此false;
②:Integer和int作比较,Integer会自动做拆箱操作转化为int类型,因此是两个数值的值比较,所以相等,因此true;
③:new生成的对象在堆中,非new生成的对象在java的常量池中,因此非new生成的Inte变量和new Integer()生成的变量比较时总为false;
④&&⑤:对呀两个非new生成的Integer对象比较时,如果两个变量值在区间-128到127之间,比较结果为true,不在这个范围的则为false;
java在编译Integer d = 100;时,会编译成Integer d = Integer.valueOf(100);,看java Api源码可以知道:
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
java对于-128到127之间的数进行缓存,如果再这个范围内不在new生成;
java接口和抽象类的主要区别
java接口和抽象类的主要区别包括接口使用关键字interface来定义,抽象类使用关键字abstract来定义;接口使用implements关键字定义其具体实现,java抽象类使用extends关键字实现继承。除以上两点以外还包括子类扩展的数量不同、属性访问控制符不同等等。
java接口和抽象类的主要区别:
1、定义关键字不同
接口使用关键字interface来定义,抽象类使用关键字abstract来定义。
2、继承或实现的关键字不同
接口使用implements关键字定义其具体实现,java抽象类使用extends关键字实现继承。
3、子类扩展的数量不同
接口的实现类可以有多个,而抽象类的子类,只能继承一个抽象类,在Java语言中,一个类只能继承一个父类(单继承),但可以实现多个接口。
4、属性访问控制符不同
接口中属性的访问控制符只能是public,接口中的属性默认是public static final修饰的。抽象类中的属性访问控制符无限制,可为任意控制符。
5、方法控制符不同
接口中方法的默认控制符是public,并且不能定义为其他控制符,抽象类中的方法控制符无限制,其中抽象方法不能使用private修饰。
6、方法实现不同
接口中普通方法不能有具体的方法实现,在JDK 8之后static和default方法必须有方法实现,static或default方法如果没有方法实现就会报错,而普通方法如果有方法实现就会报错。
抽象类中普通方法可以有方法实现,抽象方法不能有方法实现,抽象类中的普通方法如果没有方法实现就会报错,而抽象方法如果有方法实现则会报错。
7、静态代码块使用不同
接口中不能使用静态代码块,抽象类中可以使用静态代码块。
super和this的区别:
super: 它引用当前对象的最近的父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,子类与父类中有相同成员定义时如:super.变量名 super.成员函数据名(实参)
this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名)
调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。
this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。
从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。
集合相关:
Java中的集合类有哪些?说说它们的区别。
一、集合
1.1、集合概念
集合就是一个放数据的容器,准确的说是放数据对象引用的容器
集合类存放的都是对象的引用,而不是对象的本身
集合类型主要有3种:set(集)、list(列表)和map(映射)。
1.2、集合特点
①集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。
②和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小
1.3、常用的集合类
Map接口和Collection接口是所有集合框架的父接口:
Collection接口的子接口包括:Set接口和List接口
Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
1.4、集合和数组的区别
①数组是固定长度的;集合可变长度的。
②数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
③数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。
1.5、List,Set,Map三者的区别?
① List (突出顺序): 存储的元素是有序的、可重复的; ② Set (注重独⼀⽆⼆): 存储的元素是⽆序的、不可重复的; ③ Map: 使⽤键值对(kye-value)存储,类似于数学上的函数 y=f(x),“x”代表key,"y"代表 value,Key 是⽆序的、不可重复的,value 是⽆序的、可重复的,每个键最多映射到⼀个值。
Collection 接⼝下⾯的集合
① List 接口
Arraylist : Object[] 数组, Vector : Object[] 数组, LinkedList : 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)。
② Set 接口
HashSet (⽆序,唯⼀): 基于 HashMap 实现的,底层采⽤ HashMap 来保存元素;
LinkedHashSet : LinkedHashSet 是 HashSet 的⼦类,并且其内部是通过 LinkedHashMap 来 实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现⼀样,不过还是 有⼀点点区别的;
TreeSet (有序,唯⼀): 红⿊树(⾃平衡的排序⼆叉树)。
③ Map 接口
HashMap : JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要 为了解决哈希冲突⽽存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较⼤的变 化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时 间;
LinkedHashMap : LinkedHashMap 继承⾃ HashMap ,所以它的底层仍然是基于拉链式散列结构 即由数组和链表或红⿊树组成。另外, LinkedHashMap 在上⾯结构的基础上,增加了⼀条双向链 表,使得上⾯的结构可以保持键值对的插⼊顺序。同时通过对链表进⾏相应的操作,实现了访问顺 序相关逻辑; Hashtable : 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突⽽存 在的;
TreeMap : 红⿊树(⾃平衡的排序⼆叉树)。
如何选择使用集合
① 根据键值获取元素值时就选⽤ Map 接⼝下的集合,需要排序时选择 TreeMap ,不需要排序时选择 HashMap ,保证线程安全就选⽤ConcurrentHashMap ; ② 只需要存放元素值时,就选择实现 Collection 接⼝的集合,保证元素唯⼀时选择实现Set 接⼝的集合⽐如 TreeSet 或 HashSet ,不需要就选择实现 List 接⼝的⽐如 ArrayList 或LinkedList ,然后再根据实现这些接⼝的集合的特点来选⽤。
为什么要使用集合
① 在实际开发中,存储的数据的类型是多种多样的,数组支持的类型较固定,就出现了“集合”;
② 数组的缺点是⼀旦声明之后,⻓度就不可变了,数组存储的数据是有序、可重复的,特点单⼀;
③ 集合提⾼数据存储灵活性,Java 集合不仅⽤来存储不同类型不同数量对象,还可以保存具有映射关系的数据。
集合线程安全选择
常⽤的 Arraylist , LinkedList , Hashmap , HashSet , TreeSet , TreeMap , PriorityQueue 都 不是线程安全的。如果使⽤线程安全集合的话, java.util.concurrent 包中提供很多并发容器:
① ConcurrentHashMap : 可以看作是线程安全的 HashMap
② CopyOnWriteArrayList :可以看作是线程安全的 ArrayList ,在读多写少的场合性能⾮常好,远 远好于 Vector .
③ ConcurrentLinkedQueue :⾼效的并发队列,使⽤链表实现。可以看做⼀个线程安全的 LinkedList ,这是⼀个⾮阻塞队列。
④ BlockingQueue : 这是⼀个接⼝,JDK 内部通过链表、数组等⽅式实现了这个接⼝。表示阻塞队 列,⾮常适合⽤于作为数据共享的通道。
⑤ ConcurrentSkipListMap :跳表的实现。这是⼀个 Map ,使⽤跳表的数据结构进⾏快速查找。
Arraylist 和 Vector 的区别?
① ArrayList 是 List 主要实现类,底层使⽤ Object[ ]存储,适⽤于频繁的查找⼯作,线程不安全 ;
② Vector 是 List 的古⽼实现类,底层使⽤ Object[ ]存储,线程安全的。
Arraylist 与 LinkedList 区别?
① 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
② 底层数据结构: Arraylist 底层使⽤的是 Object Object 数组; LinkedList 底层使⽤的是 双向链表,数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。
③ 插⼊和删除是否受元素位置的影响:
ArrayList 采⽤数组存储,所以插⼊和删除元素的时间复杂度受元素位置的影响。 ⽐如:执⾏ add(E e) ⽅法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插⼊和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进⾏上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执⾏向后位/向前移⼀位的操作。
LinkedList LinkedList 采⽤链表存储,所以对于 add(E e) add(E e) ⽅法的插⼊,删除元素时间复杂度不受元素位置的影响,近似O(1),如果是要在指定位置 ii 插⼊和删除元素的话( (add(int index, E element) (add(int index, E element) ) 时间复杂度近似为 o(n)) o(n)) 因为需要先移动到指定位置再插⼊。
④ 是否⽀持快速随机访问: LinkedList 不⽀持⾼效的随机元素访问,⽽ ArrayList ⽀持。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) ⽅法)。
⑤ 内存空间占⽤: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留⼀定的容量空间,⽽ LinkedList 的空间花费则体现在它的每⼀个元素都需要消耗⽐ ArrayList 更多的空间(因为要存放直 接后继和直接前驱以及数据)。
comparable 和 Comparator 的区别
① comparable 接⼝实际上是出⾃ java.lang 包它有⼀个compareTo(Object obj) ⽅法⽤来排序;
② comparator 接⼝实际上是出⾃ java.util 包它有⼀个compare(Object obj1, Object obj2) ⽅法⽤ 来排序。
⽐较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
HashSet 是 Set 接⼝的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值;
LinkedHashSet 是 HashSet 的⼦类,能够按照添加的顺序遍历;
TreeSet 底层使⽤红⿊树,能够按照添加元素的顺序进⾏遍历,排序的⽅式有⾃然排序和定制排序。
HashMap 和 Hashtable 的区别
① 线程是否安全: HashMap 是⾮线程安全的,HashTable 是线程安全的,因为 HashTable 内部的⽅法基本都经过 synchronized 修饰。(如果保证线程安全就使⽤ ConcurrentHashMap);
② 效率: 因为线程安全,HashMap 要⽐ HashTable 效率⾼⼀点。另外,HashTable 基本被淘 汰,不要在代码中使⽤它;
③ 对 Null key 和 Null value 的⽀持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能⼀个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,NullPointerException。
④ 初始容量⼤⼩和每次扩充容量⼤⼩的不同 :
① 创建时如果不指定容量初始值,Hashtable 默认的初始⼤⼩为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化⼤⼩为 16。之后每次扩充,容量变为原来的 2 倍。
② 创建时如果给定了容量初始值,那么 Hashtable 会直接使⽤你给定的⼤⼩,⽽ HashMap 会将其扩充为 2 的幂次⽅⼤⼩(HashMap 中的 tableSizeFor() ⽅法保 证,下⾯给出了源代码)。也就是说 HashMap 总是使⽤ 2 的幂作为哈希表的⼤⼩,后⾯会介绍到为 什么是 2 的幂次⽅。
⑤ 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较⼤变化,当链表⻓度⼤于阈 值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏ 数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。Hashtable 没有这 样的机制。 HashMap 中带有初始容量的构造函数:
排序总结(数组排序,ArrayList排序,HashMap排序)
一.数组排序
两种方式:
(1)直接调用sort方法
String[] s = new String[10];
Arrays.sort(s);
(2)自定义sort方法
String[] s = new String[10];
Arrays.sort(s,Arrays.sort(s, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
}););
注意:
Arrays.sort()方法既能对int数组进行排序,也能对String进行排序,但是默认是从小到大排序,若要进行倒序排序,只能用自定义sort方法排序。
用自定义方法对字符串数组进行排序时,compare()方法的返回值是int类型,若返回o1-o2,则会报错。
正确做法是使用String类中的compareTo方法进行字符串比较并返回一个int值,源码如下:
二.ArrayList排序
ArrayList底层为Object数组,排序方式和数组类似。
两种方法
(1)直接调用sort方法
List<Integer> integers = new ArrayList<>();
Collections.sort(integers);
(2)自定义sort方法
List<Integer> integers = new ArrayList<>();
Collections.sort(integers,new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
}););
三.TreeSet排序
两种方法
(1)定义可排序对象,直接添加进TreeSet中
TreeSet可是采用两种方法实现排序:自然排序和定制排序。默认情况,TreeSet采用自然排序。
如果想采用自然排序,则要存储的对象所属类必须实现Comparable 接口。该接口只有一个方法public int compareTo(Object obj),必须实现该方法。
定义一个学生类,输入五个学生信息,按照学生成绩从高到底排序并输出(姓名-语文-数学-英语)
public class Student implements Comparable<Student> {
public String name;
public int chinese;
public int english;
public int math;
public int total;
public Student() {
}
public Student(String name, int chinese, int english, int math, int total) {
this.name = name;
this.chinese = chinese;
this.english = english;
this.math = math;
this.total = total;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", chinese=" + chinese +
", english=" + english +
", math=" + math +
", total=" + total +
'}';
}
@Override
public int compareTo(Student o) {
return o.total-this.total;
}
}
class StudentTest{
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
Set<Student> set = new TreeSet<Student>();
for(int i=0;i<5;i++){
System.out.println("姓名-语文-英语-数学");
String name=scanner.nextLine();
String[] strings = name.split("-"); //以-分隔字符串
int chinese=Integer.valueOf(strings[1]); //添加进treeset
int english=Integer.valueOf(strings[2]);
int math=Integer.valueOf(strings[3]);
Student student = new Student(strings[0], chinese, english, math, chinese + english + math);
set.add(student);
}
System.out.println(set);
}
}
(2)定制排序
我们可以自定义(Comparator)比较器,在创建TreeSet集合对象时把我们自定义的比较器传入,则可以TreeSet会按照我们的比较器中定义的规则进行排序。
class StudentTest{
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
Set<Student> set = new TreeSet<Student>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o2.total-o1.total;
}
});
for(int i=0;i<5;i++){
System.out.println("姓名-语文-英语-数学");
String name=scanner.nextLine();
String[] strings = name.split("-");
int chinese=Integer.valueOf(strings[1]); //添加进treeset
int english=Integer.valueOf(strings[2]);
int math=Integer.valueOf(strings[3]);
Student student = new Student(strings[0], chinese, english, math, chinese + english + math);
set.add(student);
}
System.out.println(set);
}
}
运行结果也是相同的
注意:
定义排序对象是自定义类来实现comparable接口,定制排序则是实现comparator接口,不要混淆。
实现两个接口需要重写的方法也不同,前者重写compareTo方法,后者重写compare方法。
小Demo
需求:将一个数组元素去重后排序
java int[] arr={4,2,324,6,435,12,49,21,6,21,15,88};
方法一:
将数组元素添加进TreeSet中,实现comparator接口进行自定义排序
int[] arr={4,2,324,4,435,12,6,49,6,21,15,21,88};
TreeSet<Integer> set = new TreeSet<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
for(int i=0;i<arr.length;i++){
set.add(arr[i]);
}
for (Integer i :
set) {
System.out.print(i+"\t");
}
方法二:
使用Integer,Double等常用类作为treeset的引用类型,它们已经重写过compare方法,直接添加进treeset即可。
int[] arr={4,2,324,4,435,12,6,49,6,21,15,21,88};
TreeSet<Integer> set = new TreeSet<>();
for (Integer i :
set) {
System.out.print(i+"\t");
}
注意:使用TreeSet存储数据时,引用类型必须实现comparator接口并且重写compare方法,因为在添加元素时Treeset会调用元素的compare方法。因此若元素的引用类型为自定义的类,又没有重写compare方法,程序就会报错。但是使用Integer,Double等常用类作为引用类型,它们已经重写过compare方法,可以直接使用。
总结:
在对集合进行排序之前,一定要清楚集合底层的数据结构是怎样的,进行排序时,集合又做了什么,避免不必要的错误。
ArrayList,LinkedList,Vector的区别
1.三者之间的关系
List接口:存储有序的、可重复的数据,—> “动态数据”
ArrayList:作为List接口的主要实现类,线程不安全的,执行效率高,底层使用Object[]存储
LinkedList:对于频繁的插入和删除操作,使用此类比ArrayList效率高,底层使用双向链表存储
Vector:作为List接口的古老实现类,线程安全的,执行效率低,底层使用Object[]存储
2.ArrayList和LinkedList之间的区别
(1)相同点
ArrayList和LinkedList都继承了AbstractList抽象类,都实现了List接口
ArrayList和LinkedList是两个集合类,用于存储对象引用
(2)不同点
ArrayList底层通过动态数组实现,而LinkedList底层通过双向链表实现
ArrayList根据索引查找元素的效率比较高,但是插入、删除数据时性能比较低,需要将待插入或删除元素之后的元素往后或往前移动
LinkedList查询元素的效率比较低,需要从头部或尾部开始查找,挨个遍历每一个元素直到找到所需元素,但是插入、删除元素的效率比较高,比如,删除一个元素只需要把它的前一个元素的指针指向它后一个元素就可以
3.ArrayList和Vector之间的区别
(1)相同点
ArrayList和LinkedList都继承了AbstractList抽象类,都实现了List接口
ArrayList和Vector底层都是通过数组实现的
默认初始化大小都是10
(2)不同点
Vector是线程安全的,而ArrayList是线程不安全的,Vector类中的方法通过synchronized修饰实现线程的同步,但实现同步需要很高的开销,这也导致了访问Vector比ArrayList慢
ArrayList和Vector都是采用连续的内存空间存储元素,但是当空间不足的时候,两个类扩容的方式是不同的。(ArrayList每次存储时会检查空间大小,不够时会扩充为原来的1.5倍,Vector会扩充为原来空间的2倍
HashMap、Hashtable、ConcurrentHashMap的区别
1.概述
HashMap、Hashtable、ConcurrentHashMap是日常开发中使用频率较高的数据结构,它们都是以key-value的形式来存储数据,且都实现了Map接口,日常开发中很多人对其三者之间的区别并没有十分清晰的概念。本文将剖析部分源码,以及从线程安全性、速度等方面来分析三者之间的区别。首先讲下三者的一些区别:
1.HashMap与Hashtable基本上等价,区别在于Hashtable的大部分方法都是被synchronized修饰,并且键值都不能为null(HashMap则可以);
2.由于Hashtable大部分方法被synchronized修饰,因此是线程安全的,HashMap则是非线程安全的,大量的线程存取可能会出现异常;
3.hashMap效率相对比Hashtable高,因为synchronized修饰方法,获取锁会耗费时间,导致效率相对较低。
HashMap和Hashtable的区别
1.线程安全不同
HashMap是非线程安全的,只是用于单线程环境下;
ConcurrentHashMap是线程安全的,多线程环境下可用;
Hashtable是线程安全的,能用于多线程环境中;
2.继承的父类不同
HashMap继承自AbstractMap类。但二者都实现了Map接口。
Hashtable继承自Dictionary类,Dictionary类是一个已经被废弃的类(见其源码中的注释)。父类都被废弃,自然而然也没人用它的子类Hashtable了。
3.包含的contains方法不同
HashMap是没有contains方法的,而包括containsValue和containsKey方法;
hashtable则保留了contains方法,效果同containsValue,还包括containsValue和containsKey方法。
4.是否允许null值
Hashmap是允许key和value为null值的,用containsValue和containsKey方法判断是否包含对应键值对;
HashTable键值对都不能为空,否则包空指针异常。
5.计算hash值方式不同
HashMap有个hash方法重新计算了key的hash值,因为hash冲突变高,所以通过一种方法重算hash值的方法:这里计算hash值,先调用hashCode方法计算出来一个hash值,再将hash与右移16位后相异或,从而得到新的hash值。
Hashtable通过计算key的hashCode()来得到hash值就为最终hash值。
6.解决hash冲突方式不同
HashMap中,当出现冲突时 链表+红黑树
HashTable中, 都是以链表方式存储。
02 ConcurrentHashMap与其他类的区别
2.1 与HashMap的区别是什么?
ConcurrentHashMap是HashMap的升级版,HashMap是线程不安全的,而ConcurrentHashMap是线程安全。而其他功能和实现原理和HashMap类似。
2.2 与Hashtable的区别是什么?
Hashtable也是线程安全的,但每次要锁住整个结构,并发性低。相比之下,ConcurrentHashMap获取size时才锁整个对象。
Hashtable对get/put/remove都使用了同步操作。ConcurrentHashMap只对put/remove同步。
Hashtable是快速失败的,遍历时改变结构会报错ConcurrentModificationException。ConcurrentHashMap是安全失败,允许并发检索和更新。
然后,在腾讯云社区,我还看到了一个区别,JDK8的ConcurrentHashMap和JDK7的ConcurrentHashMap的区别。
2.3 JDK8的ConcurrentHashMap和JDK7的ConcurrentHashMap有什么区别?
集合框架的线程安全问题ArrayList、HashSet、HashMap
集合框架的线程安全问题
ArrayListLinkedList,HashSet,HashMap等等,它们都是线程不安全的。因此在高并发的场景下不能使用。应该使用它们对应的并发版本
CopyOnWriteArrayList 是对应ArrayList的并发版本
CopyOnWriteArraySet是对应HashSet的并发版本
ConcurrentHashMap是对应HashMap的并发版本
使用起来非常耗时
线程安全的:Hashtable,ConcurrentHashMap,Vector ,CopyOnWriteArrayList ,CopyOnWriteArraySet
线程不安全的:HashMap,ArrayList,LinkedList,HashSet,TreeSet,TreeMap
常用到的集合有 Set、List、Map。其中set和list继承了collection接口。
一、List
常用操作
add(数据):添加数据
remove(数据):删除数据
indexOf(数据):返回数据在集合中第一次出现式的索引位置的值
contains():用于判断集合中是否包含指定的元素,有返回true。没有返回false。
clear():将list中的对象变成垃圾清空。
remove,contains,indexOf 三个方法会默认去调用equals方法。
1.1 ArrayList
ArrayList 是最常用的集合,它实现了 List 接口,继承了 AbstractList 类,由一个 Object[] 实例实现,即底层是一个数组,每个元素之间不能有间隔。
默认初始化长度为 10,扩容规则为 0.5倍的原容量加1,即一次扩容后的长度为 16
相对于LinkedList,它查询速度快,添加和删除较慢。它适合查找,不适合增删,从中间位置增删时,要对数组进行移动、复制、所费的代价比较高。当它的大小不满足时时会创建一个新数组,然后将旧数组的数据复制到新数组
线程不同步(不安全)
1.2 LinkedList:
LinkedList底层是一个双向链表,所以很适合做插入、删除的操作。LinkedList继承于AbstractSequentialList,。它还提供了List接口中没有的方法,专门用于操作表头和表尾的元素,可以当堆栈、队列、双向链表使用 但不适合读
实现了 List 接口,继承了 AbstractSequentialList 类,同时也实现了Deque,Queue接口
由一个 Node 节点链表实现,底层是一个双向链表,所以很适合做插入、删除的操作
由于其数据结构为链表,无预扩容机制;
特点:添加、删除速度快,查询相对于ArrayList较慢
线程不同步(不安全)。
LinkindeList 特有的方法
public void addFirst(E e) 将指定元素插入到次列表的开头
public void addLast(E e) 将指定元素添加到此列表的结尾
public E getFirst() 返回此列表的第一个元素
public E getLast() 返回此列表的最后一个元素
public E removeFirst() 移除并返回此列表的第一个元素
public E removeLast() 移除并返回此列表的最后一个元素
public E pop() 从此列表所表示的堆栈处弹出一个元素
public void push(E e) 将元素推入此列表所表示的堆栈
1.3 Vector:
Vector实现了 List 接口,继承了 AbstractList 类,由一个 Object[] 实例实现,即底层为数组结构;
默认初始化长度为 10,扩容加载因子为 1,当元素长度大于原容量时进行扩容,默认为 10,一次扩容后容量为 20;
特点:线程安全,但是速度慢;在实现的方法上,用 synchronized 关键字进行了修饰,即在方法上实现了同步锁。
List删除需要注意的点
在实际开发中有时候会碰到这样的场景,需要将一个list集合中的某些特定的元素给删除掉,这个时候用可以用List提供的remove方法来实现需求。
List中的remove方法传入的参数可以是集合的下标,也可以是集合中一个元素,也可以是一个集合。
错误使用示例一:
@Data
@AllArgsConstructor
public class Person {
private String id;
private String name;
public static void main(String[] args) {
List<Person> lists = new ArrayList<>();
lists.add(new Person("1", "张三"));
lists.add(new Person("2", "王五"));
lists.add(new Person("3", "李六"));
lists.add(new Person("4", "哈哈"));
for (Person person : lists) {
if (person.getId().equals("2")) {
lists.remove(person);
}
}
}
}
这里我使用的是增强for循环,会发现直接报错。
List集合的一个特点是它其中的元素时有序的,也就是说元素的下标是根据插入的顺序来的,在删除头部或者中间的一个元素后,后面的元素下标会往前移动。循环的时候就会出现问题。
解决方案一:不要用for-each遍历,换成迭代器遍历,并且不要用list.remove()方法移除对象,用迭代器的方法iterator.remove()移除对象。
@Data
@AllArgsConstructor
public class Person {
private String id;
private String name;
public static void main(String[] args) {
List<Person> lists = new ArrayList<>();
lists.add(new Person("1", "张三"));
lists.add(new Person("2", "王五"));
lists.add(new Person("3", "李六"));
lists.add(new Person("4", "哈哈"));
Iterator<Person> iterator = lists.iterator();
while (iterator.hasNext()){
Person next = iterator.next();
if(next.getId().equals("2")){
iterator.remove();
}
}
lists.forEach(item-> System.out.println(item));
}
}
解决方案二:在循环中(比如for循环)使用remove方法时,注意要从List集合的最后一个元素开始遍历。
@Data
@AllArgsConstructor
public class Person {
private String id;
private String name;
public static void main(String[] args) {
List<Person> lists = new ArrayList<>();
lists.add(new Person("1", "张三"));
lists.add(new Person("2", "王五"));
lists.add(new Person("3", "李六"));
lists.add(new Person("4", "哈哈"));
for (int i = lists.size() - 1; i >= 0; i--) {
if (lists.get(i).getId().equals("2")) {
lists.remove(i);
}
}
lists.forEach(item-> System.out.println(item));
}
}
二、 Map
Map是一个接口,存储的是键值对。Map存储的键如果重复则会覆盖值。重复的意思是hashcode和equals方法做比较,只有两个都一致则会认为是重复。
HashMap在JDK1.8的底层是(数组+链表+红黑树)
根据键的hashcode存储数据,大多是情况下可以直接定位到它的值,因而具有很快的访问速度,
遍历的顺序不确确定的。
HashMap最多只允许一条记录的键为null,允许多条记录的值为null。
HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。
常用操作方法
put():添加数据。
get(key):获取单个数据。
keySet():获得键的集合。
values():获得所有值的集合。
entrySet():获得所有的entry对象(键值的集合)。
isEmpty():判断集合是否为空
size():获得数据的个数。
remove(key):删除某个值。
Map包含:HashMap、LinkedHashMap、TreeMap、Hashtable、ConcurrentHashMap
其中Hashtable和ConcurrentHashMap是线程安全的。
2.1 HashMap:
HashMap实现了 Map接口,继承了 AbstractMap类,数据结构采用的位桶数组,底层采用链表或红黑树进行存储;链表成红黑树。
默认初始化长度为 16,扩容加载因子为 0.75,一旦大于 0.75*16之后,就会调用resize()进行扩容,扩容2倍,即32;
JDK1.7中,数据结构采用:位桶数组+链表结构;
JDK1.8中,数据结构采用:位桶数组+(链表/红黑树);
支持克隆,无序,线程不同步,非安全。
2.2 LinkedHashMap:
LinkedHashMap 实现了 Map 接口,继承了 HashMap 类;
迭代顺序由 accessOrder 属性的决定,默认为 false,以插入顺序访问;设置为 true 则按上次读取顺序访问(每次访问元素时会把元素移动到链表末尾方便下次访问,结构会时刻变化即get后会移动到末尾)。
默认初始化长度为 16,扩容加载因子为 0.75,一旦>0.75*16之后,就会调用resize()进行扩容,与HashMap一致;
支持克隆,有序,线程不同步,非安全。
2.3 TreeMap
TreeMap可以对集合中的键进行排序,首先元素自身具有比较性。如果元素不具备比较性的时候就需要使容器具备比较性。
需要定义一个类实现接口Comparator,重写compare方法,并将该接口的子类实例对象作为参数传递给TreeMap集合的构造方法。
注意:当Comparable比较方式和Comparator比较方式同时存在时,以Comparator的比较方式为主
TreeMap实现了 NavigableMap接口,继承了 AbstractMap 类;
数据结构基于红黑树实现;
该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法;
无初始化长度。
支持克隆,有序,线程不同步,非安全。
2.4 Hashtable
Hashtable实现了 Map 接口,继承了 Dictionary类;
数据结构:也是一个散列表,数据结构与HashMap相同,key、value都不可以为 null;
该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法;
默认初始化长度为 11,扩容加载因子为 0.75,一旦>0.75*11之后,就会进行扩容,扩容到2n+1,即23;
支持克隆,无序,线程同步,安全。在实现的方法上,用 synchronized 关键字进行了修饰,即在方法上实现了同步锁。
支持 Enumeration 遍历方式。
2.5 ConcurrentHashMap
ConcurrentHashMap实现了 ConcurrentMap接口,继承了 AbstractMap类;
默认初始化长度为 16,扩容加载因子为 0.75,一旦大于 0.75*16之后,就会调用resize()进行扩容,扩容2倍,即32;
数据结构:由Segment数组结构和HashEntry数组结构组成,一个ConcurrentHashMap中包含一个Segment数组,
Segment的结构和HashMap类似,是一种数组和链表结构。
使用了锁分段技术,Segment是一种可重入锁ReentrantLock,每个Segment拥有一个锁,当对HashEntry数组的数据进行修改时,必须先获得对应的Segment锁
不支持克隆,无序,线程同步,安全。
concurrentHashmap在各个版本都是线程安全的,只是1.8版本实现时做了比较大的调整,具体为:ConcurrentHashMap取消了segment分段锁,而采用CAS和synchronized来保证并发安全。数据结构采用数组+链表/红黑二叉树的方式实现。当链表中(bucket)的节点个数超过8个时,会转换成红黑树的数据结构来存储,这样设计的目的是为了减少同一个链表冲突过大情况下的读取效率。synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
三、 Set
Set集合是一个无序集合
Set中重复的数据只能添加一遍,JVM用hashcode和equals方法来判断是否重复,只有两个方法返回一致则认为是重复,先调用hashcode方法如果返回不一致则不调用equals方法,如果返回一致则调用equals方法再来判断是否一致。 所以使用时如果加的是实体对象那么实体对象要实现无重复数据要把equals和hashcode重写
Set 集合的遍历与List集合的遍历类似,只是它是无序的不能用普通for循环遍历,采用
Iterator迭代器
Iterator不是一个集合,它是一个访问List 和Set集合的方法
Iterator的基本操作有
hasNext():如果有还元素可以迭代则返回true
next():返回迭代的下一个元素。
remove():将迭代器返回的元素删除。
3.1 HashSet
HashSet存储元素的顺序并不是按照存入时的顺序而是按照哈希值来存取的。
HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。
HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能。底层数据结构是哈希表(一个元素为链表的数组,综合了数组与链表的优点)。
HashSet不是同步的;
集合元素值可以是null;
内部存储机制
当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals方法比较true,但它们的hashCode方法返回的值不相等,HashSet将会把它们存储在不同位置,依然可以添加成功。
也就是说。HashSet集合判断两个元素的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode方法返回值也相等。
靠元素重写hashCode方法和equals方法来判断两个元素是否相等,如果相等则覆盖原来的元素,依此来确保元素的唯一性
3.2 TreeSet
一个有序的Set集合,对新添加的元素按照指定的顺序排序。Integer和String对象都可以进行默认的排序,而自定义对象必须实现Comparable并重写相应的ComapreTo方法。
实现排序的两种方式:
1.Student类中实现 Comparable<T>接口
import java.util.Objects;
public class Student {
private String name;
private Integer age;
public Student(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(name, student.name) && Objects.equals(age, student.age);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
如果想倒序那么把compareTo方法改为
@Override
public int compareTo(Student o) {
//判断如果两个年龄都一样排序 如果不判断会缺失数据 如果年龄相同数据会直接丢失 感兴趣可以注释下面代码测试
if(this.age -o.age==0){
//要么返回正数要么返回负数 如果有id也可以根据id排序
return -1;
}
//按照年龄进行排序
return o.age -this.age;
}
2.重写Comparable接口中的Compareto方法 此方法会覆盖(重写)实体中实现的排序对象Comparator的compareTo方法
import java.util.Comparator;
import java.util.TreeSet;
public class TreeSetDemo02 {
public static void main(String[] args) {
TreeSet<Student> ts=new TreeSet<Student>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
//判断如果两个年龄都一样排序 如果不判断会缺失数据 如果年龄相同数据会直接丢失 感兴趣可以注释下面代码测试
if(o2.getAge() -o1.getAge()==0){
//要么返回正数要么返回负数 如果有id也可以根据id排序
return -1;
}
//按照年龄进行排序
return o2.getAge() -o1.getAge();
}
});
//创建元素对象
Student s1=new Student("zhangsan",20);
Student s2=new Student("lis",22);
Student s3=new Student("wangwu",24);
Student s4=new Student("chenliu",26);
Student s5=new Student("zhangsan",22);
Student s6=new Student("qianqi",24);
//将元素对象添加到集合对象中
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
ts.add(s6);
//遍历
for(Student s:ts){
System.out.println(s.getName()+"-----------"+s.getAge());
}
}
}
四、高并发场景你是如何使用集合的?
高并发线程安全集合主要使用CAS(实现自旋锁的基础),底层大多时间使用AQS 实现线程安全同步。
AQS:全称为AbstractQueuedSynchronized,它是JUC包中Lock锁的底层实现,可以用AQS来实现多线程的同步器。
List 集合使用JUC下的类去实现高并发情况下的集合安全 CopyOnWriteArrayList
Set 集合使用JUC下的类去实现高并发情况下的集合安全 CopyOnWriteArraySet
Map 集合使用使用JUC下的类去实现高并发情况下的集合安全 ConcurrenthashMap 例如: Map<String , String > map = new ConcurrenthashMap<>()
创建线程的几种方式
一.创建线程的4种方式
(1)写一个类继承Thread,覆盖重写run方法
(2)创建一个Runnable类型的对象,实现run()方法,传入Thread的构造方法中
(3)实现Callable接口,实现call()方法;
(4)通过Executor的工具类创建线程池,通过线程池获取线程
1.继承Thread类创建线程
通过继承Thread类来创建并启动多线程的一般步骤如下
1】d定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
2】创建Thread子类的实例,也就是创建了线程对象
3】启动线程,即调用线程的start()方法
代码实例
public class MyThread extends Thread{//继承Thread类
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
new MyThread().start();//创建并启动线程
}
}
2.实现Runnable接口创建线程
通过实现Runnable接口创建并启动线程一般步骤如下:
1】定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体
2】创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象
3】第三部依然是通过调用线程对象的start()方法来启动线程
代码实例:
public class MyThread2 implements Runnable {//实现Runnable接口
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
//创建并启动线程
MyThread2 myThread=new MyThread2();
Thread thread=new Thread(myThread);
thread().start();
//或者 new Thread(new MyThread2()).start();
}
}
3.使用Callable和Future创建线程
和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。
》call()方法可以有返回值
》call()方法可以声明抛出异常
Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。
>boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务
>V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
>V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException
>boolean isDone():若Callable任务完成,返回True
>boolean isCancelled():如果在Callable任务正常完成前被取消,返回True
介绍了相关的概念之后,创建并启动有返回值的线程的步骤如下:
1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
代码实例:
public class Main {
public static void main(String[] args){
MyThread3 th=new MyThread3();
//使用Lambda表达式创建Callable对象
//使用FutureTask类来包装Callable对象
FutureTask<Integer> future=new FutureTask<Integer>(
(Callable<Integer>)()->{
return 5;
}
);
new Thread(task,"有返回值的线程").start();//实质上还是以Callable对象来创建并启动线程
try{
System.out.println("子线程的返回值:"+future.get());//get()方法会阻塞,直到子线程执行结束才返回
}catch(Exception e){
ex.printStackTrace();
}
}
}
4.使用线程池例如用Executor框架
1.5后引入的Executor框架的最大优点是把任务的提交和执行解耦。要执行任务的人只需把Task描述清楚,然后提交即可。这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象用Executor在构造器中。
Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。
Executor接口中之定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类。ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。
ExecutorService的生命周期包括三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了的任务,当素有已经提交了的任务执行完后,便到达终止状态。如果不调用shutdown()方法,ExecutorService会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。
Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads)
创建固定数目线程的线程池。
public static ExecutorService newCachedThreadPool()
创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线 程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newSingleThreadExecutor()
创建一个单线程化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
这四种方法都是用的Executors中的ThreadFactory建立的线程,下面就以上四个方法做个比较
newCachedThreadPool()
-缓存型池子,先查看池中有没有以前建立的线程,如果有,就 reuse.如果没有,就建一个新的线程加入池中
-缓存型池子通常用于执行一些生存期很短的异步型任务
因此在一些面向连接的daemon型SERVER中用得不多。但对于生存期短的异步任务,它是Executor的首选。
-能reuse的线程,必须是timeout IDLE内的池中线程,缺省 timeout是60s,超过这个IDLE时长,线程实例将被终止及移出池。
注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止。
newFixedThreadPool(int)
-newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程
-其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子
-和cacheThreadPool不同,FixedThreadPool没有IDLE机制(可能也有,但既然文档没提,肯定非常长,类似依赖上层的TCP或UDP IDLE机制之类的),所以FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器
-从方法的源代码看,cache池和fixed 池调用的是同一个底层 池,只不过参数不同:
fixed池线程数固定,并且是0秒IDLE(无IDLE)
cache池线程数支持0-Integer.MAX_VALUE(显然完全没考虑主机的资源承受能力),60秒IDLE
newScheduledThreadPool(int)
-调度型线程池
-这个池子里的线程可以按schedule依次delay执行,或周期执行
SingleThreadExecutor()
-单例线程,任意时间池中只能有一个线程
-用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE)
一般来说,CachedTheadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Executor的首选,只有当这种方式会引发问题时(比如需要大量长时间面向连接的线程时),才需要考虑用FixedThreadPool。(该段话摘自《Thinking in Java》第四版)
Executor执行Runnable任务
通过Executors的以上四个静态工厂方法获得 ExecutorService实例,而后调用该实例的execute(Runnable command)方法即可。一旦Runnable任务传递到execute()方法,该方法便会自动在一个线程上
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestCachedThreadPool{
public static void main(String[] args){
ExecutorService executorService = Executors.newCachedThreadPool();
// ExecutorService executorService = Executors.newFixedThreadPool(5);
// ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++){
executorService.execute(new TestRunnable());
System.out.println("************* a" + i + " *************");
}
executorService.shutdown();
}
}
class TestRunnable implements Runnable{
public void run(){
System.out.println(Thread.currentThread().getName() + "线程被调用了。");
}
}
某次执行后的结果如下:
从结果中可以看出,pool-1-thread-1和pool-1-thread-2均被调用了两次,这是随机的,execute会首先在线程池中选择一个已有空闲线程来执行任务,如果线程池中没有空闲线程,它便会创建一个新的线程来执行任务。
Executor执行Callable任务
在Java 5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,但是Runnable任务没有返回值,而Callable任务有返回值。并且Callable的call()方法只能通过ExecutorService的submit(Callable<T> task) 方法来执行,并且返回一个 <T>Future<T>,是表示任务等待完成的 Future。
Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常而Callable又返回结果,而且当获取返回结果时可能会抛出异常。Callable中的call()方法类似Runnable的run()方法,区别同样是有返回值,后者没有。
当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象。同样,将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行,并且会返回执行结果Future对象,但是在该Future对象上调用get方法,将返回null。
下面给出一个Executor执行Callable任务的示例代码:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class CallableDemo{
public static void main(String[] args){
ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<String>> resultList = new ArrayList<Future<String>>();
//创建10个任务并执行
for (int i = 0; i < 10; i++){
//使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中
Future<String> future = executorService.submit(new TaskWithResult(i));
//将任务执行结果存储到List中
resultList.add(future);
}
//遍历任务的结果
for (Future<String> fs : resultList){
try{
while(!fs.isDone);//Future返回如果没有完成,则一直循环等待,直到Future返回完成
System.out.println(fs.get()); //打印各个线程(任务)执行的结果
}catch(InterruptedException e){
e.printStackTrace();
}catch(ExecutionException e){
e.printStackTrace();
}finally{
//启动一次顺序关闭,执行以前提交的任务,但不接受新任务
executorService.shutdown();
}
}
}
}
class TaskWithResult implements Callable<String>{
private int id;
public TaskWithResult(int id){
this.id = id;
}
/**
* 任务的具体过程,一旦任务传给ExecutorService的submit方法,
* 则该方法自动在一个线程上执行
*/
public String call() throws Exception {
System.out.println("call()方法被自动调用!!! " + Thread.currentThread().getName());
//该返回结果将被Future的get方法得到
return "call()方法被自动调用,任务返回的结果是:" + id + " " + Thread.currentThread().getName();
}
}
某次执行结果如下:
从结果中可以同样可以看出,submit也是首先选择空闲线程来执行任务,如果没有,才会创建新的线程来执行任务。另外,需要注意:如果Future的返回尚未完成,则get()方法会阻塞等待,直到Future完成返回,可以通过调用isDone()方法判断Future是否完成了返回。
自定义线程池
自定义线程池,可以用ThreadPoolExecutor类创建,它有多个构造方法来创建线程池,用该类很容易实现自定义的线程池,这里先贴上示例程序:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolTest{
public static void main(String[] args){
//创建等待队列
BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(20);
//创建线程池,池中保存的线程数为3,允许的最大线程数为5
ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,50,TimeUnit.MILLISECONDS,bqueue);
//创建七个任务
Runnable t1 = new MyThread();
Runnable t2 = new MyThread();
Runnable t3 = new MyThread();
Runnable t4 = new MyThread();
Runnable t5 = new MyThread();
Runnable t6 = new MyThread();
Runnable t7 = new MyThread();
//每个任务会在一个线程上执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
pool.execute(t6);
pool.execute(t7);
//关闭线程池
pool.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName() + "正在执行。。。");
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
运行结果如下:
从结果中可以看出,七个任务是在线程池的三个线程上执行的。这里简要说明下用到的ThreadPoolExecuror类的构造方法中各个参数的含义。
public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue)
corePoolSize:线程池中所保存的线程数,包括空闲线程。
maximumPoolSize:池中允许的最大线程数。
keepAliveTime:当线程数大于核心数时,该参数为所有的任务终止前,多余的空闲线程等待新任务的最长时间。
unit:等待时间的单位。
workQueue:任务执行前保存任务的队列,仅保存由execute方法提交的Runnable任务。
总结 四种创建线程方法对比
实现Runnable和实现Callable接口的方式基本相同,不过是后者执行call()方法有返回值,后者线程执行体run()方法无返回值,因此可以把这两种方式归为一种这种方式与继承Thread类的方法之间的差别如下:
1、线程只是实现Runnable或实现Callable接口,还可以继承其他类。
2、这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
3、但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。
4、继承Thread类的线程类不能再继承其他父类(Java单继承决定)。
5、前三种的线程如果创建关闭频繁会消耗系统资源影响性能,而使用线程池可以不用线程的时候放回线程池,用的时候再从线程池取,项目开发中主要使用线程池
注:在前三种中一般推荐采用实现接口的方式来创建多线程
什么是线程池?如何实现?常见参数有哪些?
线程池(Thread Pool)是一种并发编程中常用的技术,用于管理和重用线程。它由线程池管理器、工作队列和线程池线程组成。
线程池的基本概念是,在应用程序启动时创建一定数量的线程,并将它们保存在线程池中。当需要执行任务时,从线程池中获取一个空闲的线程,将任务分配给该线程执行。当任务执行完毕后,线程将返回到线程池,可以被其他任务复用。
线程池的设计思想是为了避免频繁地创建和销毁线程的开销,以及控制并发执行的线程数量,从而提高系统的性能和资源利用率。
线程池的关键组成部分包括:
线程池管理器(ThreadPoolExecutor):负责创建、管理和控制线程池。它负责线程的创建、销毁和管理,以及线程池的状态监控和调度任务。
工作队列(BlockingQueue):用于存储待执行的任务。当线程池中的线程都在执行任务时,新的任务会被放入工作队列中等待执行。
线程池线程(Worker Thread):实际执行任务的线程。线程池中会维护一组线程,这些线程可以被重复使用,从而避免了频繁创建和销毁线程的开销。
线程池的运行机制如下:
当任务到达时,线程池管理器会检查线程池中是否有空闲的线程。如果有,则将任务分配给空闲线程执行;如果没有,则进入下一步。
如果线程池中的线程数量未达到最大限制,线程池管理器会创建一个新的线程,并将任务分配给该线程执行。
如果线程池中的线程数量已达到最大限制,并且工作队列未满,则将任务放入工作队列中等待执行。
当线程池中的线程执行完任务后,会从工作队列中获取下一个任务并执行。
线程池的优点包括重用线程、控制并发度、提供线程管理和监控等。通过适当地配置线程池的大小和任务队列的容量,可以充分利用系统资源,提高程序的性能和响应速度。同时,线程池可以避免线程创建和销毁的开销,减少了资源的浪费。
线程池的主要优点包括:
重用线程:线程池会在内部维护一组可重用的线程,避免了频繁地创建和销毁线程的开销,提高了线程的利用率。
控制并发度:线程池可以限制并发执行的线程数量,防止系统过载。通过调整线程池的大小,可以控制并发度,避免资源消耗过大。
提供线程管理和监控:线程池提供了一些管理和监控机制,例如线程池的创建、销毁、线程状态的监控等,方便开发人员进行线程的管理和调试。
提供任务队列:线程池通常会使用任务队列来存储待执行的任务,这样可以实现任务的缓冲和调度。
线程池的一些缺点包括:
需要合理配置:线程池的性能和效果受到配置参数的影响,需要根据具体的应用场景和硬件环境来合理配置线程池的大小、任务队列的大小等参数。
可能引发资源泄露:如果线程池中的线程长时间闲置而不被使用,可能会导致资源的浪费和泄露。
可能引发死锁:在使用线程池时,如果任务之间存在依赖关系,可能会引发死锁问题,需要额外的注意和处理。
使用线程池的一般步骤如下:
创建线程池:使用Executors类提供的静态方法之一,或者直接使用ThreadPoolExecutor类来创建线程池。
ExecutorService executor = Executors.newFixedThreadPool(5); // 创建固定大小的线程池
- 提交任务:使用execute()或submit()方法将任务提交给线程池。
executor.execute(new MyRunnable()); // 提交Runnable任务
Future future = executor.submit(new MyCallable()); // 提交Callable任务,并返回Future对象
3. 关闭线程池:在不再需要线程池时,需要调用shutdown()方法来关闭线程池。这将停止接受新任务,并逐渐关闭线程池中的线程。
executor.shutdown();
工作中就是需要以new ThreadPoolExecutor的方式创建线程池的,其余的不安全
线程池的三大方法:
线程池的七大参数:
二、线程池的参数介绍
有很多接口丰富的线程池类,本文这里只讲一个(讲解接口最丰富的类)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
三、使用Executors 创建常见的线程池
Executors.newFixedThreadPool(int num);//创建一个固定线程数量的线程池
Executors.newCachedThreadPool();//创建出一个线程数目动态变化的线程池
Executors.newSingleThreadExecutor();//包含单个线程的线程池(比原生api更简单)
Executors.newScheduledThreadPool();//类似于定时器,可能由多个线程执行
都是依靠使用submit方法,将任务添加到线程中。
四、线程池的使用案例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(4);
service.submit(new Runnable() {
@Override
public void run() {
while(true){
System.out.println(Thread.currentThread().getName()+111);
}
}
});
service.submit(new Runnable() {
@Override
public void run() {
while(true){
System.out.println(Thread.currentThread().getName()+222);
}
}
});
}
}
运行截图:
五、线程池的模拟实现
class MyExecutor{
private int num;//线程数量
MyExecutor(int num){
this.num = num;
MyTake myTake = new MyTake();
myTake.start();
}
volatile BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
static Object object = new Object();
class MyTake extends Thread{
@Override
public void run() {
for (int i = 0; i < num; i++) {
Thread thread = new Thread() {
@Override
public void run() {
synchronized (object) {
try {
//为了使线程池一直处于运行状态
while(true){
//执行任务
while (queue.isEmpty()) {
object.wait();
}
Runnable runnable = queue.take();
runnable.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
}
}
}
public void submit(Runnable runnable){
synchronized (object){
try {
queue.put(runnable);
object.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
六、线程池的优点
1)降低资源消耗:减少线程的创建和销毁带来的性能开销;
2)提高响应速度:当任务来时可以直接使用,不用等待线程创建;
3)可管理性:进行统一的分配、监控,避免大量的线程之间因互相抢占系统资源导致的阻塞现象。