包括内容:基础知识点,集合类,队列,线程(基础),io
Java介绍
请描述一下java
java是一个面向对象的编程语言,有继承 封装 多态的特性,同时java也是一开源的语言,一次编译到处运行,有很完善的生态系统,包括各种各样的企业级框架,同时也不用我们自己管理内存问题(但是要注意内存分配),有完善的垃圾回收机制
jdk工具部分exe解释
* jar.exe jar包
* javadoc.exe 文档生成器
* Java.exe 解释器
* Javac.exe 编辑器
开发与运行Java程序需经过哪些过程?
* 用工具编辑源程序,也就是写代码,保存
* 用Java编辑器工具javac.exe编译源程序文件,生成字节码.class文件
* 用Java解释器工具Java.exe解释运行生成.class文件
Java是如何实现跨平台的?
- 如下所示:
- 跨平台是怎样实现的呢?这就要谈及Java虚拟机(Java Virtual Machine,简称 JVM)。
- JVM也是一个软件,不同的平台有不同的版本。我们编写的Java源码,编译后会生成一种 .class 文件,称为字节码文件。Java虚拟机就是负责将字节码文件翻译成特定平台下的机器码然后运行。也就是说,只要在不同平台上安装对应的JVM,就可以运行字节码文件,运行我们编写的Java程序。
- 而这个过程中,我们编写的Java程序没有做任何改变,仅仅是通过JVM这一”中间层“,就能在不同平台上运行,真正实现了”一次编译,到处运行“的目的。
- JVM是一个”桥梁“,是一个”中间件“,是实现跨平台的关键,Java代码首先被编译成字节码文件,再由JVM将字节码文件翻译成机器语言,从而达到运行Java程序的目的。
- 注意:编译的结果不是生成机器码,而是生成字节码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行。不同平台下编译生成的字节码是一样的,但是由JVM翻译成的机器码却不一样。
- 所以,运行Java程序必须有JVM的支持,因为编译的结果不是机器码,必须要经过JVM的再次翻译才能执行。即使你将Java程序打包成可执行文件(例如 .exe),仍然需要JVM的支持。
- 注意:跨平台的是Java程序,不是JVM。JVM是用C/C++开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的JVM。
类的加载过程
一个Java文件从编码完成到最终执行,一般主要包括两个过程
编译
运行
编译,即把我们写好的java文件,通过javac命令编译成字节码,也就是我们常说的.class文件。
运行,则是把编译声称的.class文件交给Java虚拟机(JVM)执行。
而我们所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。
举个通俗点的例子来说,JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。
由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。
Java中New一个对象是个怎么样的过程?
https://blog.csdn.net/qq_27686779/article/details/102942215
Java创建对象的几种方式
其实创建对象的方式有四种:
- 用new关键字创建
- 调用对象的clone方法
- 利用反射,调用Class类/Constructor类的
newInstance()
方法 - 用反序列化,调用ObjectInputStream类的
readObject()
方法
实例对象是怎样存储的?
对象的实例存储在堆空间,对象的元数据存储在方法区,对象的引用存储在栈中。
基础
String是类类型,不是基本类型。
基本类型 有八种:整型 (4种)字符型 (1种)浮点型 (2种)布尔型(1种)
关键字 | 类型 | 位数 (8位一字节) | 取值范围(表示范围) |
---|---|---|---|
byte | 整型 | 8 | -2^7 ~ 2^7-1 |
short | 整型 | 16 | -2^15 ~ 2^15-1 |
int | 整型 | 32 | -2^31 ~ 2^31-1 |
long | 整型 | 64 | -2^63 ~ 2^63-1 |
float | 浮点数 | 32 | 3.402823e+38 ~ 1.401298e-45 |
double | 浮点数 | 64 | 1.797693e+308~ 4.9000000e-324 |
char | 文本型 | 16 | 0 ~ 2^16-1 |
boolean | 布尔值 | 32/8 | true/false |
基本数据类型 && 包装类型
基本类型 | 包装器类 |
---|---|
int | Integer |
short | Short |
long | Long |
float | Float |
double | Double |
byte | Byte |
char | Character |
boolean | Boolean |
装箱:把基本类型转换成包装类,使其具有对象的性质,又可分为手动装箱和自动装箱
拆箱:和装箱相反,把包装类对象转换成基本类型的值,又可分为手动拆箱和自动拆箱
Java自带的线程安全的基本类型包括: AtomicInteger, AtomicLong, AtomicBoolean, AtomicIntegerArray,AtomicLongArray等
1.自动类型转换和强制类型转换规则
- 自动类型转换也叫隐式类型转换
- 表达式的数据类型自动提升
- 从低位类型到高位类型自动转换;从高位类型到低位类型需要强制类型转换:
- boolean类型与其他基本类型不能进行类型的转换(既不能进行自动类型的提升,也不能强制类型转换)
2.对于取值范围,在对应的包装器类中有常量记载,直接调用就可以了,无需记忆
- 基本类型byte 二进制位数:Byte.SIZE最小值:Byte.MIN_VALUE最大值:Byte.MAX_VALUE
- 基本类型short二进制位数:Short.SIZE最小值:Short.MIN_VALUE最大值:Short.MAX_VALUE
- 基本类型char二进制位数:Character.SIZE最小值:Character.MIN_VALUE最大值:Character.MAX_VALUE
- 基本类型double 二进制位数:Double.SIZE最小值:Double.MIN_VALUE最大值:Double.MAX_VALUE
3、基本类型出现的原因
在Java编程思想的第一章就讲到:万物皆对象,new一个对象存储在堆中,我们通过堆栈的引用来使用这些对象,但是对于经常用到的一系列类型如int,如果我们用new将其存储在堆里就不是很有效——特别是简单的小的变量。所以就出现了基本类型,同C++一样,Java采用了相似的做法,对于这些类型不是用new关键字来创建,而是直接将变量的值存储在堆栈中,因此更加高效。
4、包装类型出现的原因
Java是一个面向对象的语言,基本类型并不具有对象的性质,为了与其他对象“接轨”就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
float 与 double
Java 不能隐式执行向下转型,因为这会使得精度降低。
1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。Java 不能隐式执行向下转型,因为这会使得精度降低。
1// float f = 1.1;
1.1f 字面量才是 float 类型。
1float f = 1.1f;
隐式类型转换
因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。
1short s1 = 1;2// s1 = s1 + 1;
但是使用 += 运算符可以执行隐式类型转换。
1s1 += 1;
上面的语句相当于将 s1 + 1 的计算结果进行了向下转型:
1s1 = (short) (s1 + 1);
StackOverflow : Why don’t Java’s +=, -=, *=, /= compound assignment operators require casting?
&和&&的区别
& 有两个作用,分别是 位与 和 逻辑与
&& 就是逻辑与
长路与&:两侧,都会被运算
短路与&&:只要第一个是false,第二个就不进行运算了
java 包访问权限
Java中的面向接口编程
面向接口编程是很多软件架构设计理论都倡导的编程方式,学习Java自然少不了这一部分,下面是我在学习过程中整理出来的关于如何在Java中实现面向接口编程的知识。
接口体现的是一种规范和实现分离的设计哲学,充分利用接口可以极好地降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。基于这种原则,通常推荐“面向接口”编程,而不是面向实现类编程,希望通过面向接口编程来降低程序的耦合。
变量命名规范
首字母:字母、$和下划线。变量名:由$、字母、数字和下划线组成。
循环、选择结构
- if
- switch
在switch 中可以使用的类型 Java支持的数据类型有五种
他们分别是:
byte、char、short、int、enum;
以上是JDK1.6以前的版本。
JDK1.7时,又增加了String
语法
int is = 0;
switch (is) {
case 1:
System.out.println("**");
break;
case 2:
break;
default:
System.out.println();
break;
}
- while
// 先判断再执行
while (i <= 100) {
System.out.println("第" + i + "好好学习,天天向上!");
i++;
}
// 先执行再判断
do {
} while (i <= 100);
- for
for (int is = 0; is < 100; is++) {
System.out.println("好好学习!");
}
break && continue
continue 作用:跳过循环体中剩余的语句而执行下一次循环
break语句终止某个循环,程序跳转到循环块外的下一条语句
数组
public static void main(String[] args) {
//声明数组
String [] arr;
int arr1[];
//初始化数组
int arr2[]=new int[]{1,2,3,4,5};
arr2[5] = 12
String[] array1={"马超","马云","关羽","刘备","张飞"};
String[] array2=new String[]{"黄渤","张艺兴","孙红雷","小猪","牙哥","黄磊"};
String[] array=new String[5];
//查看数组的长度
int length=array1.length;
System.out.println("length: "+array1.length);
//输出数组
// System.out.println(array1);
//结果:[Ljava.lang.String;@32f22097
System.out.println("arr2: "+Arrays.toString(arr2));
//遍历数组
for (int i = 0; i < array1.length; i++) {
// System.out.println(array1[i]);
}
//int数组转成string数组
int[] array3={1,2,3,4,5,6,7,8,9,0};
String arrStrings=Arrays.toString(array3);
// System.out.println(arrStrings);
//从array中创建arraylist
ArrayList<String> arrayList=new ArrayList<String>(Arrays.asList(array1));
System.out.println(arrayList);
//数组中是否包含某一个值
String a="马超";
if (Arrays.asList(array1).contains(a)) {
System.out.println("马超在这里");
}
//将数组转成set集合
Set<String> set=new HashSet<String>(Arrays.asList(array2));
System.out.println(set);
//将数组转成list集合
List<String> list=new ArrayList<String>();
for (int i = 0; i < array2.length; i++) {
list.add(array2[i]);
}
String[] arrStrings2={"1","2","3"};
List<String > list2=java.util.Arrays.asList(arrStrings2);
System.out.println(list2);
//Arrays.fill()填充数组
int[] arr3=new int[5];
Arrays.fill(arr3, 10); //将数组全部填充10
for (int i = 0; i < arr3.length; i++) {
System.out.println(arr3[i]);
}
//数组排序
int[] arr4 = {3, 7, 2, 1, 9};
Arrays.sort(arr4);
for (int i = 0; i < arr4.length; i++) {
System.out.println(arr4[i]);
}
int[] arr5 = {3, 7, 2, 1, 9,3,45,7,8,8,3,2,65,34,5};
Arrays.sort(arr5, 1, 4); //从第几个到第几个之间的进行排序
for (int i = 0; i < arr5.length; i++) {
System.out.println(arr5[i]);
}
//复制数组
int[] arr6 = {3, 7, 2, 1};
int[] arr7=Arrays.copyOf(arr6, 10); //指定新数组的长度
//只复制从索引[1]到索引[3]之间的元素(不包括索引[3]的元素)
int[] arr8=Arrays.copyOfRange(arr6, 1, 3);
for (int i = 0; i < arr8.length; i++) {
System.out.println(arr8[i]);
}
//比较两个数组
int[] arr9 = {1, 2, 3, 4,5,6,7,8,9,0};
boolean arr10=Arrays.equals(arr6, arr9);
System.out.println(arr10);
//去重复
//利用set的特性
int[] arr11 = {1, 2, 3, 4,5,6,7,8,9,0,3,2,4,5,6,7,4,32,2,1,1,4,6,3};
Set<Integer> set2=new HashSet<Integer>();
for (int i = 0; i < arr11.length; i++) {
set2.add(arr11[i]);
}
System.out.println(set2);
int[] arr12 = new int[set2.size()];
int j=0;
for (Integer i:set2) {
arr12[j++]=i;
}
System.out.println(Arrays.toString(arr12));
}
OOP 继承 封装 多态 抽象
笔试回答即可
多态
多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,主要体现在编译时的多态性(重载),运行时的多态性(重写)
继承
继承是从已有类创建新类的过程,子类可以拥有父类的方法和变量,同时可以添加自己的方法和变量,提高了代码的可重用性
封装
封装是指隐藏对象的属性,只对外提供访问的接口,提高了代码的安全性
抽象
抽象就是将一类实体的共同特性抽象出来,封装在一个抽象类中,所以抽象在面向对象语言是由抽象类来体现的。
封装
01.什么是封装
- 封装概述
- 是指隐藏对象的属性和实现细节,仅对外提供公共访问方式,提高了代码的安全性
02.封装好处分析
- 封装好处
- 隐藏实现细节,提供公共的访问方式
- 提高代码复用性
- 提高安全性[禁止对象之间的不良交互提高模块化]
继承
就是保留父类的属性,开扩新的东西。通过子类可以实现继承,子类继承父类的所有属性,同时也可以直接添加和修改属性,提高了代码的可重用性
继承好处和弊端
- 继承的好处
- a:提高了代码的复用性
- b:提高了代码的维护性
- c:让类与类之间产生了关系,是多态的前提
- 继承的弊端
- 类的耦合性增强了。
- 开发的原则:高内聚,低耦合。
- 耦合:类与类的关系
- 内聚:就是自己完成某件事情的能力
多态
多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,主要体现在编译时的多态性(重载),运行时的多态性(重写)
01.什么是多态
- 什么是多态?
- 多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
02.多态的实现条件
- 多态实现条件?
- Java实现多态有三个必要条件:继承、重写、向上转型。
- 继承:在多态中必须存在有继承关系的子类和父类。
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
- 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
- Java实现多态有三个必要条件:继承、重写、向上转型。
多态可以大概分为两种方式:方法重载与方法重写。
- 方法重载(Overload):编译时的多态性(也就是前绑定),方法可以根据不同参数类型进行不同的调用,方法名字一致。
- 方法重写(Override):运行时的多态(也称为后绑定)。
要实现方法重写需要做:1.方法重写,也就是子类继承父类并重写了父类已经有的方法。 2.用父类型引用来引用子类型对象,这样可以实现调用同样的方法会根据子类对象的不同表示出不一样的行为。
多态性
:多态性
是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性
分为编译时的多态性
和运行时的多态性
。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性
可以解释为:当 A 系统访问 B 系统提供的服务时,B 系统有多种提供服务的方式, 但一切对 A 系统来说都是透明的。方法重载(overload)实现的是编译时的多态性
(也称为前绑定),而方法重写
(override)实现的是运行时的多态性
(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:1. 方法重写(子类继承
父类并重写父类中已有的或抽象
的方法);2. 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
4) 抽象
:抽象
是将一类对象的共同特征总结出来构造类的过程,包括数据抽象
和行为抽象
两方面。抽象
只关注对象有哪些属性和行为,并不关注这些行为的细节是什么
父类Animal
class Animal {
int num = 10;
static int age = 20;
public void eat() {
System.out.println("动物吃饭");
}
public static void sleep() {
System.out.println("动物在睡觉");
}
public void run(){
System.out.println("动物在奔跑");
}
}
子类Cat
class Cat extends Animal {
int num = 80;
static int age = 90;
String name = "tomCat";
public void eat() {
System.out.println("猫吃饭");
}
public static void sleep() {
System.out.println("猫在睡觉");
}
public void catchMouse() {
System.out.println("猫在抓老鼠");
}
}
测试类Demo_Test1
class Demo_Test1 {
public static void main(String[] args) {
Animal am = new Cat();
am.eat();
am.sleep();
am.run();
//am.catchMouse();这里先注释掉,等会会说明
//System.out.println(am.name);//这里先注释,待会说明
System.out.println(am.num);
System.out.println(am.age);
}
}
以上的三段代码充分体现了多态的三个前提,即:
1、存在继承关系Cat类继承了Animal类
2、子类要重写父类的方法子类重写(override)了父类的两个成员方法eat(),sleep()。其中eat()是非静态的,sleep()是静态的(static)。
3、父类数据类型的引用指向子类对象。
如果再深究一点呢,我们可以看看上面测试类的输出结果,或许对多态会有更深层次的认识。猜一猜上面
的结果是什么。
可以看出来
子类Cat重写了父类Animal的非静态成员方法am.eat();的输出结果为:猫吃饭。
子类重写了父类(Animal)的静态成员方法am.sleep();的输出结果为:动物在睡觉
未被子类(Cat)重写的父类(Animal)方法am.run()输出结果为:动物在奔跑
那么我们可以根据以上情况总结出多态成员访问的特点:
1成员变量
2- 编译看左边(父类),运行看左边(父类)
3成员方法
4- 编译看左边(父类),运行看右边(子类)。动态绑定
5静态方法
6- 编译看左边(父类),运行看左边(父类)。
7
8(静态和类相关,算不上重写,所以,访问还是左边的)
9只有非静态的成员方法,编译看左边,运行看右边
那么多态有什么弊端呢?
不能使用子类特有的成员属性和子类特有的成员方法。
参考:https://www.zhihu.com/question/30082151
很明显,执行强转语句Cat ct = (Cat)am;之后,ct就指向最开始在堆内存中创建的那个Cat类型的对象了。
这就是多态的魅力吧,虽然它有缺点,但是它确实十分灵活,减少多余对象的创建,不用说为了使用子类的某个方法又去重新再堆内存中开辟一个新的子类对象。
抽象类与接口
1. 抽象类
抽象类和普通类最大的区别是:
抽象类不能被实例化,需要继承抽象类才能实例化其子类。
public abstract class AbstractClassExample {
protected int x;
private int y;
public abstract void func1();
public void func2() {
System.out.println("func2");
}
}
public class AbstractExtendClassExample extends AbstractClassExample {
@Override
public void func1() {
System.out.println("func1");
}
}
2. 接口
接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。
从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。
在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。
接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。
接口的字段默认都是 static 和 final 的。
public interface InterfaceExample {
void func1();
default void func2(){
System.out.println("func2");
}
int x = 123;
// int y; // Variable 'y' might not have been initialized
public int z = 0; // Modifier 'public' is redundant for interface fields
// private int k = 0; // Modifier 'private' not allowed here
// protected int l = 0; // Modifier 'protected' not allowed here
// private void fun3(); // Modifier 'private' not allowed here
}
public class InterfaceImplementExample implements InterfaceExample {
@Override
public void func1() {
System.out.println("func1");
}
}
3. 比较
从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
4. 使用选择
使用接口:
需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
需要使用多重继承。
使用抽象类:
需要在几个相关的类中共享代码。
需要能控制继承来的成员的访问权限,而不是都为 public。
需要继承非静态static和非常量final字段。
在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
深入理解 abstract class 和 interface
When to Use Abstract Class and Interface
区别
接口
接口中可以含有 变量和方法。但是要注意,接口中的变量会被隐式地指定为public static final变量(并且只能是public static final变量,用private修饰会报编译错误),而方法会被隐式地指定为public abstract方法且只能是public abstract方法(用其他关键字,比如private、protected、static、 final等修饰会报编译错误),并且接口中所有的方法不能有具体的实现,也就是说,接口中的方法必须都是抽象方法。从这里可以隐约看出接口和抽象类的区别,接口是一种极度抽象的类型,它比抽象类更加“抽象”,并且一般情况下不在接口中定义变量。
抽象类
在了解抽象类之前,先来了解一下抽象方法。抽象方法是一种特殊的方法:它只有声明,而没有具体的实现。抽象方法的声明格式为:
abstract void fun() 抽象方法必须用abstract关键字进行修饰。如果一个类含有抽象方法,则称这个类为抽象类,抽象类必须在类前用abstract关键字修饰。因为抽象类中含有无具体实现的方法,所以不能用抽象类创建对象。 包含抽象方法的类称为抽象类,但并不意味着抽象类中只能有抽象方法,它和普通类一样,同样可以拥有成员变量和普通的成员方法
接口和抽象类的区别
1)抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;
2)抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;
3)接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
4)一个类只能继承一个抽象类,而一个类却可以实现多个接口。
5)抽象类中的方法子类必须全部实现,不然子类也是抽象类,而接口中的抽象方法子类必须全部实现。
6)抽象类是一种模板设计模式,而接口是一种行为规范。
super
访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
访问父类的成员:如果子类重写了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。
Using the Keyword super
继承相关小问题
接口是否可继承接口?
可以,比如List 就继承了接口Collection
抽象类是否可实现(implements)接口?
可以,比如 MouseAdapter鼠标监听适配器 是一个抽象类,并且实现了MouseListener接口
抽象类是否可继承实体类(concrete class)?
可以,所有抽象类,都继承了Object
Java中的内部类分类
1.1 Java中的内部类共分为四种:
- 静态内部类static inner class (also called nested class)
- 成员内部类member inner class
- 局部内部类local inner class
- 匿名内部类anonymous inner class
02.内部类概述和访问特点
- A:内部类概述:
- 把类定义在其他类的内部,这个类就被称为内部类。
- 举例:在类A中定义了一个类B,类B就是内部类。
- B:内部类访问特点
- a:内部类可以直接访问外部类的成员,包括私有。
- b:外部类要访问内部类的成员,必须创建对象。
- C:内部类作用
- 内部类作用主要实现功能的隐藏、减少内存开销,提高程序的运行速度
2.1 内部类分类及成员内部类的直接使用
- A:按照内部类位置分类
- 成员位置:在成员位置定义的类,被称为成员内部类。
- 局部位置:在局部位置定义的类,被称为局部内部类。
- B:成员内部类
- 如何在测试类中直接访问内部类的成员。
- 格式: 外部类名.内部类名 对象名 = 外部类对象.内部类对象;
2.2 成员内部类的常见修饰符及应用
- A:成员内部类的修饰符:
- private 为了保证数据的安全性
- static 为了方便访问数据
- 注意事项:
- a:静态内部类访问的外部类数据必须用静态修饰。
- b: 成员方法可以是静态的也可以是非静态的
- B:成员内部类被静态修饰后的访问方式是:
* 格式: 外部类名.内部类名 对象名 = new 外部类名.内部类名();
2.3 局部内部类访问局部变量的问题
- A: 可以直接访问外部类的成员
- B: 可以创建内部类对象,通过对象调用内部类方法,来使用局部内部类功能
- C:局部内部类访问局部变量必须用final修饰
- 为什么呢?
- 因为局部变量会随着方法的调用完毕而消失,这个时候,局部对象并没有立马从堆内存中消失,还要使用那个变量。 为了让数据还能继续被使用,就用fianl修饰,这样,在堆内存里面存储的其实是一个常量值。
- 当我们添加了final其实就是延长了生命周期 , 其实就是一个常量 , 常量在常量池中 , 在方法区中
- 为什么呢?
03.内部类和外部类联系
- 内部类和外部类联系:
- 内部类可以访问外部类所有的方法和属性,如果内部类和外部类有相同的成员方法和成员属性,内部类的成员方法调用要优先于外部类即内部类的优先级比较高(只限于类内部,在主方法内,内部类对象不能访问外部类的成员方法和成员属性),外部类只能访问内部类的静态常量或者通过创建内部类来访问内部类的成员属性和方法
04.匿名内部类
4.1 匿名内部类的格式和理解
- A:匿名内部类: 就是局部内部类的简化写法。
- B:前提: 存在一个类或者接口;这里的类可以是具体类也可以是抽象类。
- C:格式:
new 类名或者接口名(){ 重写方法; } ;
4.2 本质是什么呢?
- 是一个继承了该类或者实现了该接口的子类匿名对象。
- 匿名内部类的面试题
- A:面试题
```java
按照要求,补齐代码
interface Inter { void show(); }
class Outer { //补齐代码 }
class OuterDemo {
public static void main(String[] args) {
Outer.method().show();
}
}
要求在控制台输出”HelloWorld”
- 答案
//补齐代码
public static Inter method() {
//匿名内部类
return new Inter() {
public void show(){
System.out.println("HelloWorld") ;
} ;
}
}
```
- 思考一下:匿名内部类的生命周期怎样,和所在方法是同步吗?
05.成员内部类介绍
要创建一个成员内部类的实例,需要拥有其外部类的一个实例的引用。假设外部类为A,成员内部类为B,则创建一个B类的实例的语法规则如下:
A a = new A(); A.B b = a.new B();
那么实际案例代码如下所示
public class Outer { private int value = 10; private static int staticValue=11; protected class Nested { public int getValue() { // 可以访问外部类的非静态、静态、私有成员 return value+staticValue; } } } public static void main(String[] args) { Outer outer=new Outer(); Outer.Nested nested=outer.new Nested(); System.out.println(nested.getValue()); }
06.局部内部类介绍
- 局部内部类可以简称为局部类,局部类可以在任何代码块中声明,并且其作用域位于代码块之中。例如,可以在一个方法快、一个if语句块、一个while语句块中声明一个局部类
- 如果类的实例只在作用域内使用的话,使用局部类可以说是一个好办法
public interface Logger {
void log(String message);
}
public class Outer {
String time = LocalDateTime.now().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
public Logger getLogger() {
class LoggerImpl implements Logger {
@Override
public void log(String message) {
System.out.println(time + " : " + message);
}
}
return new LoggerImpl();
}
}
public static void main(String[] args) {
Outer outer=new Outer();
Logger logger=outer.getLogger();
logger.log("Hi");
}
泛形
什么是泛型
泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 Java语言引入泛型的好处是安全简单。
在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
泛形方法
/**
* 说明:
* 1)<E>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<E>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<E>表明该方法将使用泛型类型E,此时才可以在方法中使用泛型类型E。
* 4)与泛型类的定义一样,此处E可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*
* @param inputArray
* @param <E>
*/
public static <E> void printArray(E[] inputArray) {
// 输出数组元素
for (E element : inputArray) {
System.out.println(element + " ");
}
}
public static void main(String args[]) {
Integer[] intArray = {1, 2, 3, 4, 5};
printArray(intArray);
}
泛形类
泛形接口
泛形通配符:?
泛形的限定
我们知道使用泛型类时:如果明确参数类型,那么泛型就代表一种类型;如果使用通配符?,那么泛型就代表任意类型。但有时候我们希望指定某些类型(不是一个,也不要所有)能作为参数类型,这应该怎么办呢?
Java中利用泛型的限定解决了这个问题,即泛型的限定。我们只需要按这样的格式书写:
上限:<? extends E>表示参数类型是E及其所有子类。
下限:<? super E>表示参数类型是E及其所有超类(即父类)。
Java泛型中E、T、K、V等的含义
- E - Element (在集合中使用,因为集合中存放的是元素)(集合泛形)
- T - Type(Java 类)(接口)
- K - Key(键)
- V - Value(值)
- N - Number(数值类型)
- ? - 表示不确定的java类型
枚举
enum
的全称为 enumeration
, 是 JDK 1.5
中引入的新特性,存放在 java.lang
包中,另外到了JDK1.6
后switch
语句支持枚举类型。
它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。
- 做单例模式
- 定义常量
class EnumSingleton {
/**
* 构造方法私有化
*/
private EnumSingleton(){
}
/**
* 返回实例
* @return
*/
public static EnumSingleton getInstance() {
return Singleton.INSTANCE.getInstance();
}
/**
* 使用枚举方法实现单利模式
*/
private enum Singleton {//内部枚举类
INSTANCE;
private EnumSingleton instance;
/**
* JVM保证这个方法绝对只调用一次 Enum类内部会有一个构造函数,该构造函数只能有编译器调用
*/
Singleton() {
instance = new EnumSingleton();
}
public EnumSingleton getInstance() {
return instance;
}
}
}
// 常量 枚举类型,使用关键字enum
enum Day {
MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY
// 相当于是clas中定义的常量 并且是public static final
// 编译器还会为我们生成了两个静态方法,分别是values()和 valueOf()
}
使用关键字
enum
定义的枚举类型,在编译期后,也将转换成为一个实实在在的类,而在该类中,会存在每个在枚举类型中定义好变量的对应实例对象,如上述的MONDAY
枚举类型对应public static final Day MONDAY;
,同时编译器会为该类创建两个方法,分别是values()
和valueOf()
https://blog.csdn.net/javazejian/article/details/71333103
java类的初始化顺序
本类的初始化顺序
public class InitialOrderTest {
// 静态变量
public static String staticField = "静态变量";
// 变量
public String field = "变量";
// 静态初始化块
static {
System.out.println(staticField);
System.out.println("静态初始化块");
}
// 初始化块
{
System.out.println(field);
System.out.println("初始化块");
}
// 构造器
public InitialOrderTest() {
System.out.println("构造器");
}
public static void main(String[] args) {
new InitialOrderTest();
}
运行结果:
静态变量
静态初始化块
a
变量
初始化块
构造器
含有父类的初始化顺序
class Parent {
// 静态变量
public static String p_StaticField = "父类--静态变量";
protected int i = 1;
protected int j = 8;
// 变量
public String p_Field = "父类--变量";
// 静态初始化块
static {
System.out.println(p_StaticField);
System.out.println("父类--静态初始化块");
}
// 初始化块
{
System.out.println(p_Field);
System.out.println("父类--初始化块");
}
// 构造器
public Parent() {
System.out.println("父类--构造器");
System.out.println("i=" + i + ", j=" + j);
j = 9;
}
}
public class SubClass extends Parent {
// 静态变量
public static String s_StaticField = "子类--静态变量";
// 变量
public String s_Field = "子类--变量";
// 静态初始化块
static {
System.out.println(s_StaticField);
System.out.println("子类--静态初始化块");
}
// 初始化块
{
System.out.println(s_Field);
System.out.println("子类--初始化块");
}
// 构造器
public SubClass() {
System.out.println("子类--构造器");
System.out.println("i=" + i + ",j=" + j);
}
// 程序入口
public static void main(String[] args) {
new SubClass();
}
}
运行结果:
父类–静态变量
父类–静态初始化块
子类–静态变量
子类–静态初始化块
父类–变量
父类–初始化块
父类–构造器
子类–变量
子类–初始化块
子类–构造器
关键字
== | equals | hashcode |String StringBuffer StringBuilder
== 和 equals
缓存池
public class Main_1 {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == d);//true,缓存池
System.out.println(e == f);//false,不在缓存池
System.out.println(c == (a + b));//true
System.out.println(c.equals(a + b));//true
System.out.println(g == (a + b));//true
System.out.println(g.equals(a + b));//false
System.out.println(g.equals(a + h));//true
}
}
基本类型对应的缓冲池如下:
- boolean values true and false
- all byte values
- short values between -128 and 127
- int values between -128 and 127
- char in the range \u0000 to \u007F
默认IntegerCache.low 是-127,Integer.high是128,如果在这个区间[-128,127]内,他就会把变量i当做一个变量,放到内存中,用比较是会得出true;但如果不在这个范围内,就会去new一个Integer对象,当运用“”时,会比较Integer两个对象地址,得出false。
所以最好使用equals来比较包装类型是否相等(阿里编码规范)
== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比较,只是很多类重写了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。
equals()
对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); // true
System.out.println(x == y); // false
==
是java
提供的等于比较运算符,用来比较两个变量指向的内存地址是否相同.- 所以用”==”判断两个引用数据类型是否相等的时候,实际上是在判断两个引用是否指向同一个对象
- 而
equals()
是Object
提供的一个方法Object
中equals()
方法的默认实现就是返回两个对象==
的比较结果.但是equals()
可以被重写,所以我们在具体使用的时候需要关注equals()
方法有没有被重写. - 重写equals方法时,方法的参数为Object类型,如果不是那就变成了重载,没有效果
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
当调用 String 类型数据的 equals() 方法时,首先会判断两个字符串的引用是否相等,也就是说两个字符串引用是否指向同一个对象,是则返回true。
如果不是指向同一个对象,则把两个字符串中的字符挨个进行比较。由于 s1 和 s3 字符串都是 “hello”,是可以匹配成功的,所以最终返回 true。
两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?
因为hashCode()方法和equals()方法都可以通过自定义类重写,是可以做到equals相同,但是hashCode不同的
但是,在Object类的equals()方法中有这么一段话
翻译如下:
通常来讲,在重写这个方法的时候,也需要对hashCode方法进行重写,
以此来保证这两个方法的一致性——当equals返回true的时候,这两个对象一定有相同的hashcode.
hashCode()
public native int hashCode();
Java中的类都有一个hashCode
方法,这个方法用来生成hashCode
值,这个值是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)生成的,这个值的作用是为了提高集合类的性能,例如HashSet
、HashMap
以及HashTable
。
哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。
我们可以调用对象的hashCode方法来生成hashCode值,因为两个不同的对象可能会有相同的hashCode值,所有不能通过hashCode值来判断两个对象是否相等,但是可以直接根据hashcode值判断两个对象不等,如果两个对象的hashcode值不等,则必定是两个不同的对象。如果要判断两个对象是否真正相等,必须通过equals方法。
hashCode() && equals()
重写 equals 方法的同时也需要重写 hashCode 方法,有没有想过为什么?
假如只重写equals而不重写hashcode,那么Student类的hashcode方法就是Object默认的hashcode方法,由于默认的hashcode方法是根据对象的内存地址经哈希算法得来的,显然此时s1!=s2,故两者的hashcode不一定相等。
然而重写了equals,且s1.equals(s2)返回true,根据hashcode的规则,两个对象相等其哈希值一定相等,所以矛盾就产生了,因此重写equals一定要重写hashcode,而且从Student类重写后的hashcode方法中可以看出,重写后返回的新的哈希值与Student的两个属性有关。
以下是关于hashcode的一些规定:
- 两个对象相等,hashcode一定相等
- 两个对象不等,hashcode不一定不等
- hashcode相等,两个对象不一定相等
- hashcode不等,两个对象一定不等
https://www.cnblogs.com/skywang12345/p/3324958.html
toString()
默认返回 ToStringExample@4554617c
这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
public class ToStringExample {
private int number;
public ToStringExample(int number) {
this.number = number;
}
}
ToStringExample example = new ToStringExample(123);
System.out.println(example.toString());
ToStringExample@4554617c
String
String 被声明为 final,因此它不可被继承。
- 首先String不属于8种基本类型,String是一个对象
- String 由于使用final 修饰存储在常量区(不是new出来的)
- new出来的存储在对象存储在堆中,栈中存放的为引用地址。
在 Java 8 中,String 内部使用 char 数组存储数据。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder
来标识使用了哪种编码。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte[] value;
/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
}
String 类的常用方法都有那些?
- indexOf():返回指定字符的索引
- charAt():返回指定索引处的字符。
- replace():字符串替换。
- trim():去除字符串两端空白。
- split():分割字符串,返回一个分割后的字符串数组。
- getBytes():返回字符串的 byte 类型数组。
- length():返回字符串长度。
- toLowerCase():将字符串转成小写字母。
- toUpperCase():将字符串转成大写字符。
- substring():截取字符串。
- equals():字符串比较。
String不可变的好处
1. 可以缓存 hash 值
因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
2. String Pool 的需要
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
注意:不是string创建后就默认进入池的,请看下方intern()
3. 安全性
String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
4. 线程安全
String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
Program Creek : Why String is immutable in Java?
String Pool(缓冲池)
String a = "abc";
String b = "abc";
String c = new String("xyz");
String a = “abc”;
创建字符串的时候先查找字符串缓冲池中有没有相同的对象,如果有相同的对象就直接返回该对象的引用,如果没有相同的对象就在字符串缓冲池中创建该对象,然后将该对象的应用返回。对于这一步而言,缓冲池中没有abc这个字符串对象,所以首先创建一个字符串对象,然后将对象引用返回给a。
String b = “abc”;
这一句也是想要创建一个对象引用变量b使其指向abc这一对象。这时,首先查找字符串缓冲池,发现abc这个对象已经有了,这是就直接将这个对象的引用返回给b,此时a和b就共用了一个对象abc,不过不用担心,a改变了字符串而影响了b,因为字符串都是常量,一旦创建就没办法修改了,除非创建一个新的对象。
对象创建
String a = "abc";
String b = "abc";
String c = new String("xyz");
String d = new String("xyz");
String e = "ab" + "cd";
这个程序与上边的程序比较相似,我们分比来看一下:
1、String a = “abc”;这一句由于缓冲池中没有abc这个字符串对象,所以会创建一个对象;
2、String b = “abc”;由于缓冲池中已经有了abc这个对象,所以不会再创建新的对象;
3、String c = new String(“xyz”);由于没有xyz这个字符串对象,所以会首先创建一个xyz的对象,然后这个字符串对象由作为String的构造方法,在内存中(不是缓冲池中)又创建了一个新的字符串对象,所以一共创建了两个对象;
4、String d = new String(“xyz”);缓冲池中已有该字符串对象,则缓冲池中不再创建该对象,然后会在内存中创建一个新的字符串对象,所以只创建了一个对象;
5、String e = ”ab” + ”cd”;由于常量的值在编译的时候就被确定了。所以这一句等价于String e = ”abcd”;所以创建了一个对象;
所以上面五个创建的对象的个数分别是:1,0,2,1,1。
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = "ab";
System.out.println(s3 == s4);// true
String s5 = "a" + "b";
System.out.println(s3 == s5);// true 为什么等于true,因为两者都是常量所以在编译阶段就已经能确定了
String s6 = s1 + s2;
System.out.println(s3 == s6);// false s1 s2是一个变量,所以不能提前确定他的值,所以两者不相等
String s7 = new String("ab");
System.out.println(s3 == s7);// false
final String s8 = "a";
final String s9 = "b";
String s10 = s8 + s9;
System.out.println(s3 == s10);// true
String as = new String("ss");
String ss = new String("ss");
String bs = as;
System.out.println(as == ss);// false
System.out.println(as == bs);// true
// new Integer(123)每次都会创建一个对象
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y); // false
字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。
当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。
下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false
String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3 == s4); // true
如果是采用 “bbb” 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6); // true
在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
StackOverflow : What is String interning?
深入解析 String#intern
new String(“abc”)
使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 “abc” 字符串对象)。
“abc” 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 “abc” 字符串字面量;
而使用 new 的方式会在堆中创建一个字符串对象。
创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。
public class NewStringTest {
public static void main(String[] args) {
String s = new String("abc");
}
}
使用 javap -verbose 进行反编译,得到以下内容:
// ...
Constant pool:
// ...
#2 = Class #18 // java/lang/String
#3 = String #19 // abc
// ...
#18 = Utf8 java/lang/String
#19 = Utf8 abc
// ...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String abc
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
// ...
在 Constant Pool 中,#19 存储这字符串字面量 “abc”,#3 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。
以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。
public String(String original){
this.value = original.value;
this.hash = original.hash;
}
value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。
String, StringBuffer and StringBuilder
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
1. 可变性
String 不可变
StringBuffer 和 StringBuilder 可变
2. 线程安全
String 不可变,因此是线程安全的
StringBuilder 不是线程安全的
StringBuffer 是线程安全的,内部使用 synchronized 进行同步
StringBuffer/StringBuilder
- StringBuffer和StringBuilder都实现了AbstractStringBuilder抽象类,拥有几乎一致对外提供的调用接口;其底层在内存中的存储方式与String相同,都是以一个有序的字符序列(char类型的数组)进行存储,不同点是StringBuffer/StringBuilder对象的值是可以改变的,并且值改变以后,对象引用不会发生改变;两者对象在构造过程中,首先按照默认大小申请一个字符数组,由于会不断加入新数据,当超过默认大小后,会创建一个更大的数组,并将原先的数组内容复制过来,再丢弃旧的数组。因此,对于较大对象的扩容会涉及大量的内存复制操作,如果能够预先评估大小,可提升性能。
对于三者使用的总结:
1.如果要操作少量的数据用 String
2.单线程操作字符串缓冲区下操作大量数据 StringBuilder
3.多线程操作字符串缓冲区下操作大量数据 StringBuffer
String 和StringBuffer的区别?
String是immutable的,其内容一旦创建好之后,就不可以发生改变。
StringBuffer 是可以变长的,内容也可以发生改变
改变的原理是StringBuffer内部采用了字符数组存放数据,在需要增加长度的时候,创建新的数组,并且把原来的数据复制到新的数组这样的办法来实现。
https://blog.csdn.net/yeweiyang16/article/details/51755552
初始化:可以指定给对象的实体的初始容量为参数字符串s的长度额外再加16个字符
扩容:尝试将新容量扩为大小变成原容量的1倍+2,然后if判断一下 容量如果不够,直接扩充到需要的容量大小。
StackOverflow : String, StringBuffer, and StringBuilder
5.String不可变的好处
- 5.1 可以缓存 hash 值 博客
- 因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
- 5.2 String Pool 的需要
- 如果一个String对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
- 5.3 安全性
- String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
- 5.4 线程安全
- String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
参数传递
- Java基本数据类型传递参数时是值传递;引用类型传递参数时是引用传递。
- 值传递时,将实参的值传递一份给形参;引用传递时,将实参的地址值传递一份给形参。
- 值传递时,实参把它的值传递给对应的形参,函数接收的是原始值的一个拷贝,此时内存中存在两个相等的基本类型,即实参和形参,后面方法中的操作都是对形参这个值的修改,不影响实参的值。引用传递时,实参的引用(地址,而不是参数的值)被传递给方法中相对应的形参,函数接收的是原始值的内存地址;在方法执行中,形参和实参内容相同,指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象。
- 需要特殊考虑String,以及Integer、Double等几个基本类型包装类,它们都是immutable类型,因为没有提供自身修改的函数,每次操作都是新创建一个对象,所以要特殊对待。因为最后的操作不会修改实参,可以认为是和基本数据类型相似,为值传递。
以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中使指针引用其它对象,那么这两个指针此时指向的是完全不同的对象,在一方改变其所指向对象的内容时对另一方没有影响。
public class Dog {
String name;
Dog(String name) {
this.name = name;
}
String getName() {
return this.name;
}
void setName(String name) {
this.name = name;
}
String getObjectAddress() {
return super.toString();
}
}
public class PassByValueExample {
public static void main(String[] args) {
Dog dog = new Dog("A");
System.out.println(dog.getObjectAddress()); // Dog@4554617c
func(dog);
System.out.println(dog.getObjectAddress()); // Dog@4554617c
System.out.println(dog.getName()); // A
}
private static void func(Dog dog) {
System.out.println(dog.getObjectAddress()); // Dog@4554617c
dog = new Dog("B");
System.out.println(dog.getObjectAddress()); // Dog@74a14482
System.out.println(dog.getName()); // B
}
}
如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。
class PassByValueExample {
public static void main(String[] args) {
Dog dog = new Dog("A");
func(dog);
System.out.println(dog.getName()); // B
}
private static void func(Dog dog) {
dog.setName("B");
}
}
StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?
Object 通用方法
经典:
https://fangjian0423.github.io/2016/03/12/java-Object-method/
概览
public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native Class<?> getClass()
protected void finalize() throws Throwable {}
public final native void notify()
public final native void notifyAll()
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void wait() throws InterruptedException
getClass方法
返回当前运行时对象的Class对象
hashCode方法
该方法返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。(文章中有对hashcode的详细解释)
equals方法
如果重写了equals方法,通常有必要重写hashCode方法,这点已经在hashCode方法中说明了。
clone方法
创建并返回当前对象的一份拷贝。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
toString方法
Object对象的默认实现,即输出类的名字@实例的哈希码的16进制。
notify方法:
唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。
notifyAll方法
跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
wait(long timeout) throws InterruptedException方法
wait方法会让当前线程等待直到另外一个线程调用对象的notify或notifyAll方法,或者超过参数设置的timeout超时时间。
wait(long timeout, int nanos) throws InterruptedException方法
跟wait(long timeout)方法类似,多了一个nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。
wait() throws InterruptedException方法
需要注意的是 wait(0, 0)和wait(0)效果是一样的,即一直等待。
跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念。
finalize方法
该方法的作用是实例被垃圾回收器回收的时候触发的操作,就好比 “死前的最后一波挣扎”。
补充:什么是Native Method
简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern “C”告知C++编译器去调用一个C的函数。
clone()
1. cloneable
clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
public class CloneExample {
private int a;
private int b;
}
CloneExample e1 = new CloneExample();
// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'
重写 clone() 得到以下实现:
public class CloneExample {
private int a;
private int b;
@Override
public CloneExample clone() throws CloneNotSupportedException {
return (CloneExample)super.clone();
}
}
CloneExample e1 = new CloneExample();
try {
CloneExample e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
java.lang.CloneNotSupportedException: CloneExample
以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。
应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
public class CloneExample implements Cloneable {
private int a;
private int b;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
2. 浅拷贝
拷贝对象和原始对象的引用类型引用同一个对象。
public class ShallowCloneExample implements Cloneable {
private int[] arr;
public ShallowCloneExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}
public void set(int index, int value) {
arr[index] = value;
}
public int get(int index) {
return arr[index];
}
@Override
protected ShallowCloneExample clone() throws CloneNotSupportedException {
return (ShallowCloneExample) super.clone();
}
}
ShallowCloneExample e1 = new ShallowCloneExample();
ShallowCloneExample e2 = null;
try {
e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 222
3. 深拷贝
拷贝对象和原始对象的引用类型引用不同对象。
public class DeepCloneExample implements Cloneable {
private int[] arr;
public DeepCloneExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}
public void set(int index, int value) {
arr[index] = value;
}
public int get(int index) {
return arr[index];
}
@Override
protected DeepCloneExample clone() throws CloneNotSupportedException {
DeepCloneExample result = (DeepCloneExample) super.clone();
result.arr = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
result.arr[i] = arr[i];
}
return result;
}
}
DeepCloneExample e1 = new DeepCloneExample();
DeepCloneExample e2 = null;
try {
e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 2
4. clone() 的替代方案
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
public class CloneConstructorExample {
private int[] arr;
public CloneConstructorExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}
public CloneConstructorExample(CloneConstructorExample original) {
arr = new int[original.arr.length];
for (int i = 0; i < original.arr.length; i++) {
arr[i] = original.arr[i];
}
}
public void set(int index, int value) {
arr[index] = value;
}
public int get(int index) {
return arr[index];
}
}
CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1);
e1.set(2, 222);
System.out.println(e2.get(2)); // 2
异常
运行时异常:
NullPointerException 空指针异常
ArithmeticException 算术异常,比如除数为零
ClassCastException 类型转换异常
ConcurrentModificationException同步修改异常,遍历一个集合的时候,删除集合的元素,就会抛出该异常
IndexOutOfBoundsException 数组下标越界异常
NegativeArraySizeException 为数组分配的空间是负数异常
Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。
Throwable又派生出Error类和Exception类。
错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
Error和Exception有什么区别?
Error和Exception都实现了Throwable接口
Error指的是JVM层面的错误,比如内存不足OutOfMemoryError
Exception 指的是代码逻辑的异常,比如下标越界OutOfIndexException
throw与throws的区别
通过上面的两个demo可以得知:
1、throw用在方法体内,上面代码显示了,是直接在main方法体内
throws用在方法声明后面,表示再抛出异常,由该方法的调用者来处理。这个看上面的代码就理解了
2、throw是具体向外抛异常的,抛出的是一个异常实例
throws声明了是哪种类型的异常,使它的调用者可以捕获这个异常
3、throw,如果执行了,那么一定是抛出了某种异常了,安生throws表示可能出现,但不一定。
4、同时出现的时候,throws出现在函数头、throw出现在函数体,两种不会由函数去处理,真正的处理由函数的上层调用处理
throws 函数声明
throws声明:如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异常,否则编译不通过。
throws是另一种处理异常的方式,它不同于try…catch…finally,throws仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。
采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。
public void foo() throws ExceptionType1, ExceptionType2, ExceptionTypeN {
//foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。
}
throw
public class demo {
public static void main(String[] args) {
// TODO Auto-generated method stub
String s = "abc";
if(s.equals("abc")) {
throw new NumberFormatException();
} else {
System.out.println(s);
}
}
}
for 中try还是for外try
try放在for循环的里面所有的for循环都会执行,当遇到异常时,抛出异常继续执行;放在外面,当遇到异常时,抛出异常,后面的循环就会终止,并不会执行。
构造函数器/Constructor是否可被override?是否可以继承String类?
子类不能继承父类的构造方法,所以就不存在重写父类的构造方法。
String是final修饰的,所以不能够被继承
try catch finally 执行顺序
try里的return和finally里的return 都会支持,但是当前方法只会采纳finally中return的值
https://www.cnblogs.com/superFish2016/p/6687549.html
总结以上测试:
1、finally语句总会执行
2、如果try、catch中有return语句,finally中没有return,那么在finally中去修改除了包装类型和静态变量、全局变量以外的数据都不会对try、catch中返回的变量有任何的影响(包装类型、静态变量会改变、全局变量)。但是修改包装类型和静态变量、全局变量,会改变变量的值。
3、尽量不要在finally中使用return语句,如果使用的话,会忽略try、catch中的返回语句,也会忽略try、catch中的异常,屏蔽了错误的发生。
4、finally中避免再次抛出异常,一旦finally中发生异常,代码执行将会抛出finally中的异常信息,try、catch中的异常将被忽略。
所以在实际项目中,finally常常是用来关闭流或者数据库资源的,并不额外做其他操作。
Java中如何进行异常处理
Java异常的分类和类结构图
在Java中异常的继承主要有两个: Error和Exception 这两个,而Error就是jvm出现错误,以及系统奔溃等现象这些错误没办法通过程序来处理,所以在程序中不能使用catch来捕捉处理这类的异常。
对于Exception 又可以分为checkedException 和RuntimeException 这两种异常,checkedException异常在进行编译运行之前就可以知道会不会发生异常,如果不对这些异常进行抛出、捕获的话就不能通过编译。而RuntimeException就是运行的时候出现的异常在之前你是没办法确定是不是会出现异常。
Java的异常处理是通过5个关键字来实现的:try、catch、 finally、throw、throws
try…catch…finally语句块
try{
//try块中放可能发生异常的代码。
//如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。
//如果发生异常,则尝试去匹配catch块。
}catch(Exception exception){
//每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。
//catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。
//在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。
//如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。
//如果try中没有发生异常,则所有的catch块将被忽略。
}catch(Exception exception){
//...
}finally{
//finally块通常是可选的。
//无论异常是否发生,异常是否匹配被处理,finally都会执行。
//一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。
//finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。
}
需要注意的地方
1、try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使用。
2、每一个catch块用于处理一个异常。异常匹配是按照catch块的顺序从上往下寻找的,只有第一个匹配的catch会得到执行。匹配时,不仅运行精确匹配,也支持父类匹配,因此,如果同一个try块下的多个catch异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每个catch块都有存在的意义。
3、java中,异常处理的任务就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方去。也就是说:当一个函数的某条语句发生异常时,这条语句的后面的语句不会再执行,它失去了焦点。执行流跳转到最近的匹配的异常处理catch代码块去执行,异常被处理完后,执行流会接着在“处理了这个异常的catch代码块”后面接着执行。
有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handling(恢复式异常处理模式 )
而Java则是让执行流恢复到处理了异常的catch块后接着执行,这种策略叫做:termination model of exception handling(终结式异常处理模式)
执行顺序
- 不管有没有出现异常,finally块中代码都会执行;
- 当try和catch中有return时,finally仍然会执行;
- finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,不管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的;
- finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。
举例:
情况1. try{} catch(){}finally{} return;
显然程序按顺序执行。
情况2. :try{ return; }catch(){} finally{} return;
程序执行try块中return之前(包括return语句中的表达式运算)代码;
再执行finally块,最后执行try中return;
finally块之后的语句return,因为程序在try中已经return所以不再执行。
情况3. :try{ } catch(){return;} finally{} return;
程序先执行try,如果遇到异常执行catch块,
有异常:则执行catch中return之前(包括return语句中的表达式运算)代码,再执行finally语句中全部代码,
最后执行catch块中return. finally之后也就是4处的代码不再执行。
无异常:执行完try再finally再return.
情况4:try{ return; }catch(){} finally{return;}
程序执行try块中return之前(包括return语句中的表达式运算)代码;
再执行finally块,因为finally块中有return所以提前退出。
情况5:try{} catch(){return;}finally{return;}
程序执行catch块中return之前(包括return语句中的表达式运算)代码;
再执行finally块,因为finally块中有return所以提前退出。
情况6:try{ return;}catch(){return;} finally{return;}
程序执行try块中return之前(包括return语句中的表达式运算)代码;
有异常:执行catch块中return之前(包括return语句中的表达式运算)代码;
则再执行finally块,因为finally块中有return所以提前退出。
无异常:则再执行finally块,因为finally块中有return所以提前退出。
最终结论:任何执行try 或者catch中的return语句之前,都会先执行finally语句,如果finally存在的话。
如果finally中有return语句,那么程序就return了,所以finally中的return是一定会被return的,
编译器把finally中的return实现为一个warning。
也就是说,如果try catch中有return 就等待fianlly执行完成后再去执行其他的(finally 么有ret)如果try catch中有return 就等待fianlly执行完成后再去执行其他的(finally 么有ret)
下面是个测试程序
public class FinallyTest{
public static void main(String[] args) {
System.out.println(new FinallyTest().test());;
}
static int test(){
int x = 1;
try{
x++;
return x;
}finally{
++x;
}
}
}
结果是2。
在try语句中,在执行return语句时,要返回的结果已经准备好了,就在此时,程序转到finally执行了。
在转去之前,try中先把要返回的结果存放到不同于x的局部变量中去,执行完finally之后,在从中取出返回结果,
因此,即使finally中对变量x进行了改变,但是不会影响返回结果。
它应该使用栈保存返回值。
finally中的return 会覆盖 try 或者catch中的返回值。
关键字
final
修饰类:表示该类不能被继承
修饰方法:表示该方法不能被重写
修饰变量:表示该变量只能被赋值一次
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
对于基本类型,final 使数值不变;
对于引用类型,final 使引用不变,表示该引用只有一次指向对象的机会,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
- 如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。
final关键字的好处:
(1)final关键字提高了性能。JVM和Java应用都会缓存final变量。
(2)final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
(3)使用final关键字,JVM会对方法、变量及类进行优化。
final原理
final通过反射是否可以修改
finally
finally 是用于异常处理的场面,无论是否有异常抛出,都会执行
finalize
finalize是Object的方法,所有类都继承了该方法。 当一个对象满足垃圾回收的条件,并且被回收的时候,其finalize()方法就会被调用
instanceof
它的作用是什么?
- instanceof是Java的一个二元操作符,和==,>,<是同一类东西。由于它是由字母组成的,所以也是Java的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回boolean类型的数据。
使用过程中注意事项有哪些?
- 类的实例包含本身的实例,以及所有直接或间接子类的实例
- instanceof左边显式声明的类型与右边操作元必须是同种类或存在继承关系,也就是说需要位于同一个继承树,否则会编译错误
//比如下面就会编译错误 String s = null; s instanceof null s instanceof Integer
static
在《Java编程思想》P86页有这样一段话:
static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。
这段话虽然只是说明了static方法的特殊之处,但是可以看出static关键字的基本作用,简而言之,一句话来描述就是:方便在没有创建对象的情况下来进行调用(方法/变量)
1. 静态变量
静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它;静态变量在内存中只存在一份。
实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
public class A {
private int x; // 实例变量
private static int y; // 静态变量
public static void main(String[] args) {
// int x = A.x; // Non-static field 'x' cannot be referenced from a static context
A a = new A();
int x = a.x;
int y = A.y;
}
}
2. 静态方法
静态方法在类加载的时候就存在了,它不依赖于任何实例,不能被重写。所以静态方法必须有实现,也就是说它不能是抽象方法(abstract)。
public abstract class A {
public static void func1(){
}
// public abstract static void func2(); // Illegal combination of modifiers: 'abstract' and 'static'
}
只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字。
public class A {
private static int x;
private int y;
public static void func1(){
int a = x;
// int b = y; // Non-static field 'y' cannot be referenced from a static context
// int b = this.y; // 'A.this' cannot be referenced from a static context
}
}
3. 静态语句块
静态语句块在类初始化时运行一次。
public class A {
static {
System.out.println("123");
}
public static void main(String[] args) {
A a1 = new A();
A a2 = new A();
}
}
4. 静态内部类
非静态内部类依赖于外部类的实例,而静态内部类不需要。
当一个内部类没有使用static修饰的时候,是不能直接使用内部类创建对象,须要先使用外部类对象.new内部类对象及(外部类对象.new 内部类())
而静态内部类只需要new OuterClass.InnerClass();
public class OuterClass {
class InnerClass {
}
static class StaticInnerClass {
}
public static void main(String[] args) {
// InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context
OuterClass outerClass = new OuterClass();
InnerClass innerClass = outerClass.new InnerClass();
StaticInnerClass staticInnerClass = new StaticInnerClass();
}
}
静态内部类不能访问外部类的非静态的变量和方法。
5. 静态导包
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
import static com.xxx.ClassName.*
6. 初始化顺序
存在继承的情况下,初始化顺序为:
父类(静态变量、静态语句块)
子类(静态变量、静态语句块)
父类(实例变量、普通语句块)
父类(构造函数)
子类(实例变量、普通语句块)
子类(构造函数)
static关键字的特点
- 随着类的加载而加载
- 优先于对象存在
- 被类的所有对象共享
- 可以通过类名调用【静态修饰的内容一般我们称其为:与类相关的,类成员】
- static的注意事项
- 在静态方法中是没有this关键字的
- 静态是随着类的加载而加载,this是随着对象的创建而存在。
- 静态比对象先存在。博客
- 静态方法只能访问静态的成员变量和静态的成员方法【静态只能访问静态,非静态可以访问静态的也可以访问非静态的】
static变量存储位置
- 注意是:存储在JVM的方法区中
- static变量在类加载时被初始化,存储在JVM的方法区中,整个内存中只有一个static变量的拷贝,可以使用类名直接访问,也可以通过类的实例化对象访问,一般不推荐通过实例化对象访问,通俗的讲static变量属于类,不属于对象,任何实例化的对象访问的都是同一个static变量,任何地放都可以通过类名来访问static变量。
transient
- 通过在属性前面加上transient关键字,限制属性写入到文件或网络中
- 还可以在未实现系列化接口的引用类型属性前面加上transient关键字,避免对此类属性进行递归系列化时出现java.io.NotSerializableException异常
transient String name;//添加一个transient修饰的属性
什么被序列化
属性(包括基本数据类型、数组、对其它对象的引用)
类名
什么不被序列化
static的属性
方法
加了transient修饰符的属性
hascode
public native int hashCode();
根据这个方法的声明可知,它是一个本地方法,它的实现与本地机器有关,该方法返回一个int类型的数值,并且是本地方法,因此在Object类中并没有给出具体的实现。
对于包含容器类型的程序设计语言来说,基本上都会涉及到hashCode。在Java中也一样,hashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及Hashtable,如果没有很好的覆写键的hashcode()和equals()方法,那么将无法正确的处理键。
也许大多数人都会想到调用equals方法来逐个进行比较,这个方法确实可行。但是如果集合中已经存在一万条数据或者更多的数据,如果采用equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的作用就体现出来了,当我们向一个集合中添加某个元素,集合会首先调用hashCode方法,这样就可以直接定位它所存储的位置,若该处没有其他元素,则直接保存。若该处已经有元素存在,就调用equals方法来匹配这两个元素是否相同,相同则不存,不同则散列到其他位置(具体情况请参考(Java提高篇()—–HashMap))。这样处理,当我们存入大量元素时就可以大大减少调用equals()方法的次数,极大地提高了效率。
所以hashCode在上面扮演的角色为寻域(寻找某个对象在集合中区域位置)。hashCode可以将集合分成若干个区域,每个对象都可以计算出他们的hash码,可以将hash码分组,每个分组对应着某个存储区域,根据一个对象的hash码就可以确定该对象所存储区域,这样就大大减少查询匹配元素的数量,提高了查询效率。
System.currentTimeMillis()
System位于java.lang包下,有很多可以获取到系统底层的东西:
System类本意就代表系统,系统级的很多属性和控制方法都放置在该类的内部。该类位于java.lang包。
currentTimeMillis方法
public static long currentTimeMillis()
该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和GMT时间(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数。
可以直接把这个方法强制转换成date类型。
代码如下:
long currentTime = System.currentTimeMillis();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy年-MM月dd日-HH时mm分ss秒");
Date date = new Date(currentTime);
System.out.println(formatter.format(date));
运行结果如下:
当前时间:2017年-12月19日-10时14分28秒
另:
可获得当前的系统和用户属性:
String osName = System.getProperty(“os.name”);
String user = System.getProperty(“user.name”);
System.out.println(“当前操作系统是:” + osName);
System.out.println(“当前用户是:” + user);
System.getProperty 这个方法可以得到很多系统的属性。
heap和stack有什么区别
heap: 堆
stack: 栈
- 存放的内容不一样:
- heap: 是存放对象的
- stack: 是存放基本类型(int, float, boolean 等等)、引用(对象地址)、方法调用
- 存取方式不一样:
- heap: 是自动增加大小的,所以不需要指定大小,但是存取相对较慢
- stack: 先入后出的顺序,并且存取速度比较快
反射
每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 Class.forName("com.mysql.jdbc.Driver")
这种方式来控制类的加载,该方法会返回一个 Class 对象。
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:
Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;
Constructor :可以用 Constructor 创建新的对象。
反射的优点:
可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。
反射的缺点:
尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。
性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
Trail: The Reflection API
深入解析 Java 反射(1)- 基础
什么是java序列化,如何实现java序列化?
序列化指的是把一个Java对象,通过某种介质进行传输,比如Socket输入输出流,或者保存在一个文件里。
实现java序列化的手段是让该类实现接口 Serializable,这个接口是一个标识性接口,没有任何方法,仅仅用于表示该类可以序列化。
JAVA序列化ID问题
https://blog.csdn.net/qq_35370263/article/details/79482993
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。清单 1 中,虽然两个类的功能代码完全一致,但是序列化 ID 不同,他们无法相互序列化和反序列化。
集合框架(容器)
什么是集合
通常情况下,把具有相同性质的一类东西,汇聚成一个整体,就可以称为集合。
什么是集合框架
集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法。
- 接口:即表示集合的抽象数据类型。
- 实现:也就是集合框架中接口的具体实现。
- 算法:在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方法,例如查找、排序等。
Java集合框架图。
1) 首先查看jdk中Collection类的源码后:
public interface Collection<E> extends Iterable<E> {
}
通过查看可以发现Collection是一个接口类,其继承了java迭代接口Iterable
Collection接口中的方法如下:
List
List 三个子类的区别
ArrayList:底层使用object[]数组实现,内存地址都是连续的便于索引,查询快;在新增的时候需要申请一块连续的内存空间,所以增删比较慢。
LinkedList: 底层是基于链表实现,链表内存是散乱的,在储存自身内存地址的
同时,还储存着下一个元素的内存地址,所以查询慢,增删快。
Vector:底层数组实现,由于所有的方法都是采用synchronize,线程安全,效率慢。
ArrayList
Jdk1.7之前ArrayList默认大小是10 ,JDK1.7之后是0(JDK1.8首次new的时候为0,当第一次添加的时候扩容为10),所以当new ArrayList<>()时,默认的是一个空对象,没有长度,JDK差异,每次约按1.5倍扩容。 JDK8 中ArrayList扩容的实现是通过grow()方法里使用语句newCapacity = oldCapacity + (oldCapacity >> 1)(即1.5倍扩容)计算容量,然后调用Arrays.copyof()方法进行对原数组进行复制。
若想要一个高性能,又是线程安全的ArrayList,可以使用Collections.synchronizedList(list);方法或者使用CopyOnWriteArrayList集合
ArrayList底层其实就是一个Object类型的数组,非线程安全的集合。查询元素快,插入,删除中间元素慢,初始化的长度为10,每次扩容为原大小的1.5倍,可以通过构造方法改变初始容量大小。
ArrayList快在下标定位,慢在数组复制。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
由于每次添加的时候,通过扩容机制判断原数组是否还有空间,若没有则重新实例化一个空间更大的新数组,把旧数组的数据拷贝到新数组中,耗费时间和性能
扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
CopyOnWriteArrayList
1. 读写分离
写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
写操作需要加锁,防止并发写入时导致写入数据丢失。
写操作结束之后需要把原始数组指向新的复制数组。
2. 适用场景
CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。
但是 CopyOnWriteArrayList 有其缺陷:
内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。
Vector
基于数组实现的线程安全的集合。线程同步(方法被synchronized修饰),性能比ArrayList差。
Vector 的数据结构和使用方法与ArrayList差不多。最大的不同就是Vector是线程安全的。几乎所有的对数据操作的方法都被synchronized关键字修饰。synchronized是线程同步的,当一个线程已经获得Vector对象的锁时,其他线程必须等待直到该锁被释放。从这里就可以得知Vector的性能要比ArrayList低。
若想要一个高性能,又是线程安全的ArrayList,可以使用Collections.synchronizedList(list);方法或者使用CopyOnWriteArrayList集合
LinkedList
基于双向循环链表实现的非线程安全的集合。查询元素慢,插入,删除中间元素快。
使用 Node 存储链表节点信息。
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
每个链表存储了 first 和 last 指针:
transient Node<E> first;
transient Node<E> last;
数组是将元素在内存中连续存储的;它的优点:因为数据是连续存储的,内存地址连续,所以在查找数据的时候效率比较高;它的缺点:在存储之前,我们需要申请一块连续的内存空间,并且在编译的时候就必须确定好它的空间的大小。在运行的时候空间的大小是无法随着你的需要进行增加和减少而改变的,当数据量比较大的时候,有可能会出现越界的情况,数据比较小的时候,又有可能会浪费掉内存空间。在改变数据个数时,增加、插入、删除数据效率比较低链表是动态申请内存空间,不需要像数组需要提前申请好内存的大小,链表只需在用的时候申请就可以,根据需
要来动态申请或者删除内存空间,对于数据增加和删除以及插入比数组灵活。还有就是链表中数据在内存中可以在任意的位置,通过应用来关联数据(就是通过存在元素的指针来联系)
Stack
Stack 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。
几种集合的区别
ArrayList查询快,写数据慢;LinkedList查询慢,写数据快
ArrayList查询快是因为底层是由数组实现,new的内存地址是连续的,通过下标定位数据快。写数据慢是因为复制数组耗时。LinkedList底层是双向循环链表,查询数据依次遍历慢。写数据只需修改指针引用。
ArrayList和LinkedList都不是线程安全的,小并发量的情况下可以使用Vector,若并发量很多,且读多写少可以考虑使用CopyOnWriteArrayList。
遍历list集合
@Test
public void test(){
List<String> list = new ArrayList<String>();
list.add("one");
list.add("two");
/**
* for循环遍历
*/
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
/**
* foreach遍历
*/
for (String str : list) {
System.out.println(str);
}
/**
* 通过迭代器遍历
*/
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
Set
https://blog.csdn.net/qq_33642117/article/details/52040345
在选择的时候,如果顺序很重要,则可以选择TreeSet,如果操作性能和时间效率很重要的话,则可以选择HashSet
HashSet集合
HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得,顺序不同
当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置,存取速度比较快。
- 底层数据结构是哈希表(是一个元素为链表的数组) + 红黑树,底层实际上是一个HashMap。
- 所以可以直接总结出:HashSet实际上就是封装了HashMap,操作HashSet元素实际上就是操作HashMap。这也是面向对象的一种体现,重用性贼高!
- 无序,允许为null,,有且仅有一个元素为null!
- 线程不安全的。
- HashSet还有一个子类LinkedHashSet
HashSet到底是如何判断两个元素重复。
原来,HashSet是将数据存放到HashMap的Key中!HashMap是key-value形式的数据结构,它的key是唯一的。而HashSet原理就是利用HashMap key唯一的特点。
HashMap中如何实现key的唯一性?
当key-value键值对插入到HashMap中。
首先是对key值哈希,得到key在哈希数组中的下标。HashMap中哈希是调用对象key的hashCode()方法,然后再做异或处理。
如果当前下标上已经有值,说明有两个元素的映射到相同的下标。此时,需要第二次判断,调用key对象的equals方法,对象不相同则插入链表的表尾,对象相同则覆盖。
HashSet底层实现HashMap add时添加的类型
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
key为添加的值,value为一个object对象。
TreeSet集合
- 底层数据结构是红黑树(是一个自平衡的二叉树)
- 有排序功能,支持两种排序方法:自然排序和定制排序。默认情况下,TreeSet采用自然排序。
- 有序,不允许为null
- 线程不安全的。
- 当要排序的为一个对象时,需要通过重写进行排序
排序
既然TreeSet可以自然排序,那么TreeSet必定是有排序规则的。
让存入的元素自定义比较规则。
给TreeSet指定排序规则。
方式一:元素自身具备比较性
元素自身具备比较性,需要元素实现Comparable接口,重写compareTo方法,也就是让元素自身具备比较性,这种方式叫做元素的自然排序也叫做默认排序。
方式二:容器具备比较性
当元素自身不具备比较性,或者自身具备的比较性不是所需要的。那么此时可以让容器自身具备。需要定义一个类实现接口Comparator,重写compare方法,并将该接口的子类实例对象作为参数传递给TreeMap集合的构造方法。
注意:当Comparable比较方式和Comparator比较方式同时存在时,以Comparator的比较方式为主;
注意:在重写compareTo或者compare方法时,必须要明确比较的主要条件相等时要比较次要条件。(假设姓名和年龄一直的人为相同的人,如果想要对人按照年龄的大小来排序,如果年龄相同的人,需要如何处理?不能直接return 0,因为可能姓名不同(年龄相同姓名不同的人是不同的人)。此时就需要进行次要条件判断(需要判断姓名),只有姓名和年龄同时相等的才可以返回0.)
LinkedHashSet集合
- 迭代是有序的
- 允许为null,有且仅有一个元素为null!
- 底层实际上是一个HashMap+双向链表实例(其实就是LinkedHashMap)
- 线程不安全的。
- 性能比HashSet差一丢丢,因为要维护一个双向链表
- 初始容量与迭代无关,LinkedHashSet迭代的是双向链表
适用场景分析
HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。
遍历set集合
@Test
public void test2() {
Set<String> set = new HashSet<>();
set.add("one");
set.add("two");
/**
* 迭代器遍历方式
*/
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
/**
* 遍历
*/
for (String str: set) {
System.out.println(str);
}
}
set 和list的区别
- List接口:存储一组不唯一,有序的对象
- Set接口:存储一组唯一,无序的对象
Map
https://www.javazhiyin.com/54393.html
Map接口不是继承Collection接口;Map接口用于维护键/值对(key/value pairs),他的实现类有:
Map 常用方法
HashMap
底层是哈希表数据结构,线程是不同步的,可以存入null键,null值。要保证键的唯一性,需要覆盖hashCode方法,和equals方法。
写代码优化:new HashMap<>() 指定长度规则,需要保存的长度/加载因子+1 ,加载因子=0.75
查询时间复杂度:HashMap的本质可以认为是一个数组,数组的每个索引被称为桶,每个桶里放着一个单链表,一个节点连着一个节点。很明显通过下标来检索数组元素时间复杂度为O(1),而且遍历链表的时间复杂度是O(n),所以在链表长度尽可能短的前提下,HashMap的查询复杂度接近O(1)
- 数组:存储区间连续,占用内存严重,寻址容易,插入删除困难;
- 链表:存储区间离散,占用内存比较宽松,寻址困难,插入删除容易;
- Hashmap综合应用了这两种数据结构,实现了寻址容易,插入删除也容易。
和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。
参数 | 含义 |
---|---|
capacity | table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。 |
size | 键值对数量。 |
threshold | size 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。 |
loadFactor | 加载因子,table 能够使用的比例,threshold = (int)(capacity* loadFactor)。 |
HashMap的底层实现
JDK1.8之前
JDK1.8 之前 HashMap 由 数组+链表 组成的(“链表散列” 即数组和链表的结合体),数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(HashMap 采用 “拉链法也就是链地址法” 解决冲突),如果定位到的数组位置不含链表(当前 entry 的 next 指向 null ),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度依然为 O(1),因为最新的 Entry 会插入链表头部(Java 7存入链表头部,Java 8存入链表尾部),即需要简单改变引用链即可,而对于查找操作来讲,此时就需要遍历链表,然后通过 key 对象的 equals 方法逐一比对查找.
所谓 “拉链法” 就是将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8之后
相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
Hashtable
! 从Hashtable的命名规范就可以看出,t没有大写,并不是我写错了
数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的,不可以存入null键null值。该集合是线程同步的。
它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
HashMap 和Hashtable的区别
- HashMap是线程不安全的,效率高
- Hashtable是线程安全的,效率低
- HashMap可以存储null键和null值
- Hashtable不可以存储null键和null值
TreeMap
底层是二叉树数据结构。线程不同步。可以用于给map集合中的键进行排序。
LinkedHashMap:
该子类基于哈希表又融入了链表。可以Map集合进行增删提高效率。
LinkedHashMap是一个根据某种规则有序的hashmap。根据名字,我们也可以看出这个集合是有hash散列的功能的同时也有顺序。hashmap是无法根据某种顺序来访问数据的,例如放入集合的元素先后的顺序。list都有这个功能,可以根据放入集合的先后来访问具体的数据。
https://www.javazhiyin.com/34466.html
ConcurrentHashMap
1.8 之前ConcurrentHashMap
底层采用 分段的数组+链表 实现,该类是线程安全的 Map
实现。与 Hashtable
相似,但 Hashtable
的 synchronized
是针对整张Hash
表的,即每次锁住整张表让线程独占,ConcurrentHashMap
允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对 hash 表的不同部分进行的修改。ConcurrentHashMap
内部使用段(Segment
)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。
到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表/红黑树的数据结构来实现,并发控制使用 synchronized
和 CAS
、volatile
来操作。(JDK1.6
以后 对 synchronized
锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap
,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
- 同时在
jdk1.8
中加锁只是针对head节点,不影响其他元素的读写,锁的粒度更细,扩容是,阻塞所有的读写操作、并发扩容
JDK1.7的ConcurrentHashMap:
JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点;Node: 链表节点):
https://www.cnblogs.com/huangjuncong/p/9478505.html
遍历map集合
@Test
public void test3() {
Map<String, String> map = new HashMap<String, String>();
map.put("one", "1");
map.put("two", "2");
map.put("three", "3");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
/**
* 第二种
*/
//遍历map中的键
for (String key : map.keySet()) {
System.out.println("Key = " + key);
}
//遍历map中的值
for (String value : map.values()) {
System.out.println("Value = " + value);
}
/**
* 迭代器
*/
Iterator<Map.Entry<String, String>> entries = map.entrySet().iterator();
while (entries.hasNext()) {
Map.Entry<String, String> entry = entries.next();
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
/**
* 通过键找值遍历(效率低)
*/
for (String key : map.keySet()) {
String value = map.get(key);
System.out.println("Key = " + key + ", Value = " + value);
}
}
并发容器
容器指的是集合框架
同步容器是通过syncrhoized关键字对线程不安全的操作进行加锁来保证线程安全的,其原理是使得多线程轮流获取同步锁进行对集合的操作,所以性能有所下降。,像Vector、Hashtable、Stack。
为此,java.util.concurrent提供了多种并发容器,以:在原有集合的拷贝上进行操作,用修改后的集合替换原集合 的方式来达到并发且安全地使用集合类的目的。
根据接口的类型,主要有以下四种接口,其他具体的容器均是对这些接口的实现类:
Queue类型:阻塞队列BlockingQueue、非阻塞队列ConcurrentLinkedQueue
Map类型:ConcurrentMap
Set类型:ConcurrentSkipListSet、CopyOnWriteArraySet
List类型:CopyOnWriteArrayList
CopyOnWrite容器
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。
集合面试问题
Collection 和 Collections的区别
Collection是接口,是List和Set的父接口
Collections是工具类,提供了排序,混淆等等很多实用方法
Comparable 接口和 Comparator 接口有什么区别?
答:
详细可以看:https://blog.csdn.net/u011240877/article/details/53399019
对于一些普通的数据类型(比如 String, Integer, Double…),它们默认实现了Comparable 接口,实现了 compareTo 方法,我们可以直接使用。
而对于一些自定义类,它们可能在不同情况下需要实现不同的比较策略,我们可以新创建 Comparator 接口,然后使用特定的 Comparator 实现进行比较。
对比:
Comparable简单,但是如果需要重新定义比较类型时,需要修改源代码。
Comparator不需要修改源代码,自定义一个比较器,实现自定义的比较方法。
案例:
// 进行排序,对已经选择的课程优先展示 PublicClassPojo 需要序列化
Collections.sort(classInfoList, new Comparator<PublicClassPojo>() {
@Override
public int compare(PublicClassPojo pp, PublicClassPojo pp2) {
Integer chooseState = pp.getChooseState();
Integer chooseState2 = pp2.getChooseState();
if (chooseState.equals(chooseState2)) {
return 0;
} else {
return chooseState > chooseState2 ? -1 : 1;
}
}
});
// 排序完成
Comparator<SubjectPojo> com = new Comparator<SubjectPojo>() {
@Override
public int compare(SubjectPojo o1, SubjectPojo o2) {
return o1.getCode().compareTo(o2.getCode());
}
};
Set<SubjectPojo> set = new TreeSet<>(com);
Java 集合的快速失败机制 “fail-fast”
答:
它是 java 集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
可以知道在进行add,remove,clear等涉及到修改集合中的元素个数的操作时,modCount就会发生改变(modCount ++),所以当另一个线程(并发修改)或者同一个线程遍历过程中,调用相关方法使集合的个数发生改变,就会使modCount发生变化
每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。
解决办法:
在遍历过程中,所有涉及到改变 modCount 值得地方全部加上 synchronized;
使用 CopyOnWriteArrayList 来替换 ArrayList。
java.util.concurrent包中包含的并发集合类如下:
详细:http://raychase.iteye.com/blog/1998965
1ConcurrentHashMap23CopyOnWriteArrayList45CopyOnWriteArraySet
java日期处理 数学处理
Date 类
Date类用来指定日期和时间,其构造函数及常用方法如下:
public Date()从当前时间构造日期时间对象。
public String toString()转换成字符串。
public long getTime()它包含的是一个长整型数据long, 表示的是从GMT(格林尼治标准时间)1970年, 1 月 1日00:00:00这一刻之前或者是之后经历的毫秒数.
DateFormat类
通过向SimpleDateFormat 的构造函数传递格式字符串“yyyy-MM-dd”,
yyyy是年,MM是月,dd是日. 字符的个数决定了日期是如何格式化的.传递“yy-MM-dd”会显示 13-2-22
Calendar类
Calendar类主要用于完成日期字段之间的相互操作的功能
Calendar类是一个抽象基类,不能new,我们可以通过Calendar.getInstance得到其实例,下面列出Calendar的一些常用方法:
set(int year, int month, int date)设置日历字段
set(int field, int value) 将给定的日历字段设置为给定值
get(int field)返回给定日历字段的值
getTime()返回一个表示此 Calendar 时间值(从历元至现在的毫秒偏移量)的 Date 对象。
如果是1则代表的是对年份操作,2是对月份操作,3是对星期操作,5是对日期操作,11是对小时操作,12是对分钟操作,13是对秒操作,14是对毫秒操作。例如:Calendar calendar = Calendar.getInstance(); calendar .add(5,1);则表示对日期进行加一天操作
Calendar示例
Math类
- abs()返回某数字的绝对值.
- ceil()会找到下一个最大整数。
- floor()返回紧邻的最小整数。
- max()返回两个值中的最大值。
- min()返回两个值中的最小值。
- random()返回一个随机数,在0.0到1.0之间的双精度数。
- round()返回与某浮点数值最接近的整数值。
- sqrt()返回某数值的平方根。
Random类
Random类 伪随机数产生器。
常用方法:
- public boolean nextBoolean()
该方法的作用是生成一个随机的boolean值。 - public double nextDouble()
该方法的作用是生成一个随机的double值,数值介于[0,1.0)之间。 - public int nextInt()
该方法的作用是生成一个随机的int值。 - public int nextInt(int n)
该方法的作用是生成一个随机的int值,该值介于[0,n)的区间,也就是0到n之间的随机int值,包含0而不包含n。
实现代码
public static void main(String[] args) {
/**
* 将日期解析为指定格式的字符串
*/
Date date = new Date();
System.out.println(date);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(simpleDateFormat.format(date));
/**
* 将字符串解析为日期对象
*/
SimpleDateFormat sim = new SimpleDateFormat("yyyy-MM-dd");
String ss = "2020-01-01";
try {
Date parse = sim.parse(ss);
System.out.println(parse.getTime());
System.out.println(sim.format(parse));
} catch (ParseException e) {
e.printStackTrace();
}
/**
* Calendar使用
*/
Calendar calendar = Calendar.getInstance();
Date time = calendar.getTime();
System.out.println(time);
/**
* 设置日期
*/
calendar.set(2019,1,31);
/**
* Random随机数
*/
Random random = new Random();
int i = random.nextInt(10);
System.out.println(i);
}
Pattern类
io流
Java中使用IO(输入输出)来读取和写入,读写设备上的数据、硬盘文件、内存、键盘……,根据数据的走向可分为输入流和输出流,这个走向是以内存为基准的,即往内存中读数据是输入流,从内存中往外写是输出流。
Java 的 I/O 大概可以分成以下几类:
- 磁盘操作:File
- 字节操作:InputStream 和 OutputStream
- 字符操作:Reader 和 Writer
- 对象操作:Serializable
- 网络操作:Socket
- 新的输入/输出:NIO
IO 介绍
我们通常所说的 BIO 是相对于 NIO 来说的,BIO 也就是 Java 开始之初推出的 IO 操作模块,BIO 是 BlockingIO 的缩写,顾名思义就是阻塞 IO 的意思。
根据处理的数据类型可分为字节流和字符流
1.字节流可以处理所有数据类型的数据,在java中以Stream结尾
2.字符流处理文本数据,在java中以Reader和Writer结尾。
字节流
输入流 InputStream
API
- int read( ) 读取一个字节,返回值为所读的字节
- int read( byte b[ ] ) 读取多个字节,放置到字节数组b中,通常读取的字节数量为b的长度,返回值为实际读取的字节的数量
- int read( byte b[ ], int off, int len ) 读取len个字节,放置到以下标off开始字节数组b中,返回值为实际读取的字节的数量
- int available( ) 返回值为流中尚未读取的字节的数量
- long skip( long n ) 读指针跳过n个字节不读,返回值为实际跳过的字节数量
- close( ) 流操作完毕后必须关闭
代码
@Test
public void test3() {
InputStream inputStream = null;
try {
inputStream = new FileInputStream("D:/test/java3.txt");
int i = 0;
// 将读取到的一个字节给i,中文占两个字节
while ((i = inputStream.read()) != -1) {
System.out.println((char) i);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
// 关闭流
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出流 OutputStream
API
- void write( int b ); //往流中写一个字节b
- void write( byte b[ ] ); //往流中写一个字节数组b
- void write( byte b[ ], int off, int len ); 把字节数组b中从下标off开始,长度为len的字节写入流中
- flush( ) 刷空输出流,并输出所有被缓存的字节由于某些流支持缓存功能,该方法将把缓存中所有内容强制输出到流中。
- close( ) 流操作完毕后必须关闭
@Test
public void test4() {
// 没有则会创建文件
File file2 = new File("D:/test/java.txt");
OutputStream os = null;
try {
os = new FileOutputStream(file2);
os.write("超哥".getBytes());
}catch (Exception e){
e.printStackTrace();
}finally {
try {
// 关闭流
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
字符流
输入流 Reader
API
- int read() throws IOException; 读取一个字符,返回值为读取的字符
- int read(char cbuf [ ]) throws IOException; 读取一系列字符到数组cbuf[]中,返回值为实际读取的字符的数量
- abstract int read( char cbuf[ ] , int off , int len) throws IOException; 读取len个字符,从数组cbuf[]的下标off处开始存放,返回值为实际读取的字符数量,该方法必须由子类实现
- boolean markSupported( ); 判断当前流是否支持做标记
- void mark ( int readAheadLimit ) throws IOException; 给当前流作标记,最多支持readAheadLimit个字符的回溯。
- void reset( ) throws IOException; 将当前流重置到做标记处
- abstract void close( ) throws IOException; 关闭
代码
@Test
public void test5() {
File file2 = new File("D:/test/java.txt");
Reader os = null;
try {
os = new FileReader(file2);
int i = 0;
while ((i = os.read()) != -1) {
System.out.println((char) i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 关闭流
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出流 Writer
API
- void write (int c) throws IOException; 将整型值c的低16位写入输出流
- void write ( String str ) throws IOException; 将字符串str中的字符写入输出流
- void write( char cbuf[ ] ) throws IOException; 将字符数组cbuf[]写入输出流
- abstract void write( char cbuf[ ] , int off , int len) throws IOException; 将字符数组cbuf[]中的从索引为off的位置处开始的len个字符写入输出流
- void write( String str , int off , int len) throws IOException; 将字符串str 中从索引off开始处的len个字符写入输出流
- flush( ) 刷空输出流,并输出所有被缓存的字节。
- close( ) 关闭流
字节、字符流转换类
字节转为字符流
字符 不需要转为字节流,字节流可以读取任意数据,而字符读取内容有限。
@Test
public void test6() {
File file = new File("D:/test/java.txt");
try {
// 将该文件以字节流的方式读取
FileInputStream fis = new FileInputStream(file);
// 将字节流转换为字符流
InputStreamReader isr = new InputStreamReader(fis);
int i = 0;
while ((i = fis.read()) != -1) {
System.out.println((char) i);
}
System.out.println(i);
fis.close();
} catch (Exception e) {
System.err.println("error");
}
}
缓冲流
缓冲流介绍
普通的字节、字符流都是一个字节一个字符这样读取的,而缓冲流则是将数据先缓冲起来,然后一起写入或者读取出来。
缓冲流为I/O流增加了内存缓冲区,使用缓冲流的好处是,能够更高效的读写信息。缓冲流要“套接”在相应的节点流(低级流)之上,对读写的数据提供了缓冲的功能。
缓冲输入流支持其父类的mark()和reset()方法:mark()用于“标记”当前位置,就像加入了一个书签,可以使用reset()方法返回这个标记重新读取数据
@Test
public void test7() {
File file = new File("D:/test/java3.txt");
try {
// 将该文件以字节流的方式读取
FileInputStream fis = new FileInputStream(file);
// 字节缓冲流
BufferedInputStream bis = new BufferedInputStream(fis);
int i = 0;
while ((i = bis.read()) != -1) {
System.out.println((char) i);
}
fis.close();
} catch (Exception e) {
System.err.println("error");
}
}
J2SDK提供了四种缓存流:
BufferedInputStream
BufferedOutputStream
BufferedReader
BufferedWriter
缓冲流字节流
java.io.BufferedInputStream类可以对任何的InputStream流进行带缓冲的封装以达到性能的改善。该类在已定义输入流上再定义一个具有缓冲的输入流,可以从此流中成批地读取字符而不会每次都引起直接对数据源的读操作。数据输入时,首先被放入缓冲区,随后的读操作就是对缓冲区中的内容进行访问
java.io.BufferedOutputStream不直接写入输出流,先写入缓冲区,当缓冲区满时,字节数据才会写到BufferedOutputStream所连接的流,调用该类的flush()将缓冲区全部写入输出流
序列化
对象序列化概述
一般地,对象不能脱离应用程序。但有时候,需要将对象的状态保存下来,在需要时再将对象恢复,即对象持久化(Persistence)。对象序列化(Object Serialization)可以将对象存储到外存中或以二进制形式通过网络传输。对象反串行化可以从这些数据中重构一个与原始对象状态相同的对象
为了实现对象系列化,对应的类必须实现下面的两种接口之一:
Serializable
Externalizable
将对象保存到磁盘文件
- 通过java.io.ObjectOutputStream可以将对象输出到磁盘文件、网络等设备
- 调用这个类的writeObject()方法,可以向特定的文件或网络输出对象
- writeObject()方法序列化指定的对象,并遍历该对象对其它对象的引用,递归的序列化所有被引用到的其它对象,从而建立一个完整的序列化流
@Test
public void test8() {
File file = new File("D:/test/java2.txt");
ObjectOutputStream oos = null;
try {
OutputStream os = new FileOutputStream(file);
oos = new ObjectOutputStream(os);
// Student需要序列化
Student student = new Student();
student.setI(1);
student.setName("tom");
oos.writeObject(student);
} catch (Exception e) {
e.printStackTrace();
}
}
从磁盘读出保存的对象
- 通过java.io.ObjectInputStream对象可以从磁盘文件中读出保存的对象(或从网络中读出传递的对象)
- 调用这个类的readObject()方法,从特定的设备读出对象readObject()方法反序列化输入流中的对象,遍历该对象中所有对其它对象的引用,并递归的反序列化这些引用对象
- readObject()方法返回的是Object对象,所以,需要对它进行必要的(向下)造型操作。
@Test
public void test9() {
File file = new File("D:/test/java2.txt");
InputStream is = null;
try {
is = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(is);
Student student = (Student) ois.readObject();
System.out.println(student.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
BIO、NIO、AIO的区别
https://my.oschina.net/u/3471412/blog/2966696
- BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。它的优点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
- NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
- AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
Socket
Socket又称“套接字”,应用程序通常通过“套接字”向网络发出请求或者应答网络请求。
java高级
JAVA 锁
类型
乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为
别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数
据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),
如果失败则要重复读-比较-写的操作。
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人
会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。
java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,
才会转换为悲观锁,如 RetreenLock。
自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁
的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗 cpu 的,说白了就是让 cpu 在做无用功,如果一直获取不到锁,那线程
也不能一直占用 cpu 自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁
的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来
说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合
使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cup 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁;
自旋锁时间阈值(1.6 引入了适应性自旋锁)
自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择
自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!
Synchronized 同步锁
synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重
入锁
ReentrantLock
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完
成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等
避免多线程死锁的方法。
AtomicInteger
首先说明,此处 AtomicInteger ,一个提供原子操作的 Integer 的类,常见的还有
AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他们的实现原理相同,
区别在与运算对象类型的不同。令人兴奋地,还可以通过 AtomicReference
有操作转化成原子操作。
我们知道,在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。
通常我们会使用 synchronized 将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些
同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger
的性能是 ReentantLock 的好几倍。
可重入锁(递归锁)
本文里面讲的是广义上的可重入锁,而不是单指 JAVA 下的 ReentrantLock。可重入锁,也叫
做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受
影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是 可重入锁。
ReadWriteLock 读写锁
为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如
果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写
锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。
读锁
如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁
写锁
如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。
总之,读的时候上读锁,写的时候上写锁!
Java 中 读 写 锁 有 个 接 口 java.util.concurrent.locks.ReadWriteLock , 也 有 具 体 的 实 现
ReentrantReadWriteLock。
公平锁与非公平锁
公平锁(Fair)
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
非公平锁(Nonfair)
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
- 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
- Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁。
共享锁和独占锁
java 并发包提供的加锁模式分为独占锁和共享锁。
独占锁
独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。
独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线
程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
共享锁
共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种
乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
- AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等
待线程的锁获取模式。 - java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,
或者被一个 写操作访问,但两者不能同时进行。
重量级锁(Mutex Lock)
Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又
是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用
户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么
Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为
“重量级锁”。JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。
JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和
“偏向锁”。
轻量级锁
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。
锁升级
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,
也就是说只能从低到高升级,不会出现锁的降级)。
“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,
轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量
级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场
景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀
为重量级锁。
偏向锁
Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线
程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起
来让这个线程得到了偏护。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级
锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换
ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所
以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过,轻
量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进
一步提高性能
分段锁
分段锁也并非一种实际的锁,而是一种思想 ConcurrentHashMap 是学习分段锁的最好实践
锁优化
- 减少锁持有时间
只用在有线程安全要求的程序上加锁
- 减小锁粒度
将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。
降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是
ConcurrentHashMap。
- 锁分离
最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互
斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能,具体也请查看[高并发 Java 五]
JDK 并发包 1。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如
LinkedBlockingQueue 从头部取出,从尾部放数据
- 锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完
公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步
和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。
- 锁消除
锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这
些对象的锁操作,多数是因为程序员编码不规范引起
链表
数组
- 数组作为数据存储结构的缺陷
- 在无序数组中,搜索是低效的
- 而在有序数组中插入效率又很低
- 不管在哪一种数组中删除效率都很低
- 创建一个数组之后,它的大小又是不可变的
链表
- 链表是一种有序的列表
- 链表的内容通常存储与内存中分散的位置上
- 链表由节点组成,每一个节点的结构都相同
- 节点分为数据域和链域,数据域是存放节点的内容,链域存放的是下一个节点的指针
单向链表(Single-Linked List)
单链表是链表中结构最简单的。一个单链表的节点(Node)分为两个部分,第一个部分(data)保存或者显示关于节点的信息,另一个部分存储下一个节点的地址。最后一个节点存储地址的部分指向空值。
单向链表只可向一个方向遍历,一般查找一个节点的时候需要从第一个节点开始每次访问下一个节点,一直访问到需要的位置。而插入一个节点,对于单向链表,我们只提供在链表头插入,只需要将当前插入的节点设置为头节点,next指向原头节点即可。删除一个节点,我们将该节点的上一个节点的next指向该节点的下一个节点。
栈(stack)与堆(heap)
1.寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制;
栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(对象可能在常量池里)(字符串常量对象存放在常量池中。);
堆:存放所有new出来的对象;
静态域:存放静态成员(static定义的);
常量池:存放字符串常量和基本类型常量(public static final)。有时,在嵌入式系统中,常量本身会和其他部分分割离开(由于版权等其他原因),所以在这种情况下,可以选择将其放在ROM中 ;
非RAM存储:硬盘等永久存储空间
堆栈是一种有序表。
堆栈只允许数据自有序列表(前端)作输入、输出操作。
堆栈的存取顺序就像手枪的弹夹一样,最先压进去的子弹会被最后打出来
具有先进后出FILO(First In Last Out)的特性
堆栈的典型操作
入栈:又称压栈,是指将数据放入堆栈
出栈:将数据从堆栈中取出来
private Object[] obj;
/**
* 指向标
*/
private int pointer;
/**
* 数组的长度
*/
private int size;
/**
* 给定一个默认的长度
*/
public Stack() {
this.obj = new Object[5];
this.pointer = -1;
this.size = 5;
}
/**
* 方法的重载
*
* @param size
*/
public Stack(int size) {
this.obj = new Object[size];
this.size = size;
this.pointer = -1;
}
/**
* 判断堆栈是否已经满了
*
* @return
*/
public boolean isFull() {
/**
* 由于指向标的位置是从-1开始的,数组的下标是从0开始的
*/
return pointer == size - 1;
}
// 入栈
public void push(Object obj) {
/**
* // isFull满的
*/
if (isFull()) {
System.out.println("空间已满!!!");
// 对堆进行扩容
obj = Arrays.copyOf(this.obj, this.obj.length * 2);
/**
* // 每次加一
*/
pointer++;
/**
* 循环给数组赋值
*/
this.obj[pointer] = obj;
} else {
pointer++;
this.obj[pointer] = obj;
}
}
/**
* 出栈
*
* @return
*/
public Object pup() {
if (isEmpty()) {
System.out.println("栈空间已空!!!");
return null;
} else {
Object o = this.obj[pointer];
pointer--;
return o;
}
}
/**
* 判断堆栈是否为空
*
* @return
*/
public boolean isEmpty() {
/**
* isEmpty空的
* 如果pointer==-1就返回true
*/
return pointer == -1;
}
public static void main(String[] args) {
Stack stack = new Stack();
stack.push("a");
stack.push("b");
stack.push("c");
stack.push("d");
/**
* 出栈
*/
while (!stack.isEmpty()) {
System.out.println(stack.pup());
}
}
队列
主要是两个:队头,队尾
入队:队尾指针向后移动
出队:队头指针向后移动
值传递和引用传递
1. 形参与实参
我们先来重温一组语法:
形参:方法被调用时需要传递进来的参数,如:func(int a)中的a,它只有在func被调用期间a才有意义,也就是会被分配内存空间,在方法func执行完成后,a就会被销毁释放空间,也就是不存在了
实参:方法被调用时是传入的实际值,它在方法被调用前就已经被初始化并且在方法被调用时传入。
举个栗子:
public static void func(int a){
a=20;
System.out.println(a);
}
public static void main(String[] args) {
int a=10;//变量
func(a);
}
例子中
int a=10;中的a在被调用之前就已经创建并初始化,在调用func方法时,他被当做参数传入,所以这个a是实参。
而func(int a)中的a只有在func被调用时它的生命周期才开始,而在func调用结束之后,它也随之被JVM释放掉,,所以这个a是形参。
main方法
/**
* Java中的main()方法详解
*/
public class HelloWorld {
public static void main(String args[]) {
System.out.println("Hello World!");
}
}
一、先说类:
HelloWorld 类中有main()方法,说明这是个java应用程序,通过JVM直接启动运行的程序。
既然是类,java允许类不加public关键字约束,当然类的定义只能限制为public或者无限制关键字(默认的)。
二、再说main()方法
这个main()方法的声明为:public static void main(String args[])。必须这么定义,这是Java的规范。
为什么要这么定义,和JVM的运行有关系。
当一个类中有main()方法,执行命令“java 类名”则会启动虚拟机执行该类中的main方法。
由于JVM在运行这个Java应用程序的时候,首先会调用main方法,调用时不实例化这个类的对象,而是通过类名直接调用因此需要是限制为public static。(类名.main())
对于java中的main方法,jvm有限制,不能有返回值,因此返回值类型为void。
main方法中还有一个输入参数,类型为String[],这个也是java的规范,main()方法中必须有一个入参,类型必须String[],至于字符串数组的名字,这个是可以自己设定的,根据习惯,这个字符串数组的名字一般和sun java规范范例中mian参数名保持一致,取名为args。因此,main()方法定义必须是:“public static void main(String 字符串数组参数名[])”。
三、main()方法中可以throw Exception
因此main()方法中可以抛出异常,main()方法上也可以声明抛出异常。
四、main()方法中字符串参数数组作用
main()方法中字符串参数数组作用是接收命令行输入参数的,命令行的参数之间用空格隔开。
jdk1.8新特性学习
新特性列表
以下是Java8中的引入的部分新特性。关于Java8新特性更详细的介绍可参考这里。
- 接口默认方法和静态方法
- Lambda 表达式
- 函数式接口
- 方法引用
- Stream
- Optional
- Date/Time API
- 重复注解
- 扩展注解的支持
- Base64
- JavaFX
- 其它
- JDBC4.2规范
- 更好的类型推测机制
- HashMap性能提升
- IO/NIO 的改进
- JavaScript引擎Nashorn
- 并发(Concurrency)
- 类依赖分析器jdeps
- JVM的PermGen空间被移除
Lambda
第一个编程概念是流处理。介绍一下, 流是一系列数据项,一次只生成一项。程序可以从输
入流中一个一个读取数据项,然后以同样的方式将数据项写入输出流。一个程序的输出流很可能
是另一个程序的输入流
Java 8中增加的另一个编程概念是通过API
来传递代码的能力。这听起来实在太抽象了。在
Unix的例子里,你可能想告诉sort命令使用自定义排序。虽然sort命令支持通过命令行参数来
执行各种预定义类型的排序,比如倒序,但这毕竟是有限的
第三个编程概念更隐晦一点,它来自我们前面讨论流处理能力时说的“几乎免费的并行”。
函数式接口
行为参数化是一个很有用的模式 它能够轻松地适应不断变化的需求。这种模式可以把一个行为(一段代码)封装起来,并通过传递和使用创建的行为(例如对Apple的不同谓词)将方法的行为参数化。前面提到过,这种做法类似于策略设计模式。你可能已经在实践中用过这个模式了。 Java API中的很多方法都可以用不同的行为来参数化。这些方法往往与匿
名类一起使用。
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> result = new ArrayList<>();
for (T e : list) {
if (p.test(e)) {
result.add(e);
}
}
return result;
}
List<User> inventory = Arrays.asList(new User("green"),
new User("green"),
new User("red"));
//
List<User> redApples =
filter(inventory, (User apple) -> "red".equals(apple.getColor()));
List<Integer> evenNumbers =
filter(numbers, (Integer i) -> i % 2 == 0);
*Predicate Consumer Function *
public interface Predicate<T>{
// 它接受泛型T对象,并返回一个boolean
boolean test(T t);
}
public interface Consumer<T>{
// 定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回( void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。比如,你可以用它来创建一个forEach方法,接受一个Integers的列表,并对其中每个元素执行操作
void accept(T t);
}
public interface Function<T, R> {
// 接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象
R apply(T t);
}
@FunctionalInterface又是怎么回事?
如果你去看看新的Java API
,会发现函数式接口带有@FunctionalInterface
的标注。这个标注用于表示该接口会设计成一个函数式接口。如果你用@FunctionalInterface
定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding abstract methods found in interface Foo”
,表明存在多个抽象方法。请注意, @FunctionalInterface
不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。 它就像是@Override
标注表示方法被重写了
1. list转map
工作中,我们经常遇到list
转map
的案例。Collectors.toMap
就可以把一个list
数组转成一个Map
。代码如下:
public class TestLambda {
public static void main(String[] args) {
List<UserInfo> userInfoList = new ArrayList<>();
userInfoList.add(new UserInfo(1L, "捡田螺的小男孩", 18));
userInfoList.add(new UserInfo(2L, "程序员田螺", 27));
userInfoList.add(new UserInfo(2L, "捡瓶子的小男孩", 26));
/**
* list 转 map
* 使用Collectors.toMap的时候,如果有可以重复会报错,所以需要加(k1, k2) -> k1
* (k1, k2) -> k1 表示,如果有重复的key,则保留第一个,舍弃第二个
*/
Map<Long, UserInfo> userInfoMap = userInfoList.stream().collect(Collectors.toMap(UserInfo::getUserId, userInfo -> userInfo, (k1, k2) -> k1));
userInfoMap.values().forEach(a->System.out.println(a.getUserName()));
}
}
//运行结果
捡田螺的小男孩
程序员田螺
类似的,还有Collectors.toList()
、Collectors.toSet()
,表示把对应的流转化为list
或者Set
。
2. filter()过滤
从数组集合中,过滤掉不符合条件的元素,留下符合条件的元素。
List<UserInfo> userInfoList = new ArrayList<>();
userInfoList.add(new UserInfo(1L, "捡田螺的小男孩", 18));
userInfoList.add(new UserInfo(2L, "程序员田螺", 27));
userInfoList.add(new UserInfo(3L, "捡瓶子的小男孩", 26));
/**
* filter 过滤,留下超过18岁的用户
*/
List<UserInfo> userInfoResultList = userInfoList.stream().filter(user -> user.getAge() > 18).collect(Collectors.toList());
userInfoResultList.forEach(a -> System.out.println(a.getUserName()));
//运行结果
程序员田螺
捡瓶子的小男孩
3. foreach遍历
foreach 遍历list,遍历map,真的很丝滑。
/**
* forEach 遍历集合List列表
*/
List<String> userNameList = Arrays.asList("捡田螺的小男孩", "程序员田螺", "捡瓶子的小男孩");
userNameList.forEach(System.out::println);
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("公众号", "捡田螺的小男孩");
hashMap.put("职业", "程序员田螺");
hashMap.put("昵称", "捡瓶子的小男孩");
/**
* forEach 遍历集合Map
*/
hashMap.forEach((k, v) -> System.out.println(k + ":\t" + v));
//运行结果
捡田螺的小男孩
程序员田螺
捡瓶子的小男孩
职业: 程序员田螺
公众号: 捡田螺的小男孩
昵称: 捡瓶子的小男孩
4. groupingBy分组
提到分组,相信大家都会想起SQL
的group by
。我们经常需要一个List做分组操作。比如,按城市分组用户。在Java8之前,是这么实现的:
List<UserInfo> originUserInfoList = new ArrayList<>();
originUserInfoList.add(new UserInfo(1L, "捡田螺的小男孩", 18,"深圳"));
originUserInfoList.add(new UserInfo(3L, "捡瓶子的小男孩", 26,"湛江"));
originUserInfoList.add(new UserInfo(2L, "程序员田螺", 27,"深圳"));
Map<String, List<UserInfo>> result = new HashMap<>();
for (UserInfo userInfo : originUserInfoList) {
String city = userInfo.getCity();
List<UserInfo> userInfos = result.get(city);
if (userInfos == null) {
userInfos = new ArrayList<>();
result.put(city, userInfos);
}
userInfos.add(userInfo);
}
而使用Java8的groupingBy
分组器,清爽无比:
Map<String, List<UserInfo>> result = originUserInfoList.stream()
.collect(Collectors.groupingBy(UserInfo::getCity));
5. sorted+Comparator 排序
工作中,排序的需求比较多,使用sorted+Comparator
排序,真的很香。
List<UserInfo> userInfoList = new ArrayList<>();
userInfoList.add(new UserInfo(1L, "捡田螺的小男孩", 18));
userInfoList.add(new UserInfo(3L, "捡瓶子的小男孩", 26));
userInfoList.add(new UserInfo(2L, "程序员田螺", 27));
/**
* sorted + Comparator.comparing 排序列表,
*/
userInfoList = userInfoList.stream().sorted(Comparator.comparing(UserInfo::getAge)).collect(Collectors.toList());
userInfoList.forEach(a -> System.out.println(a.toString()));
System.out.println("开始降序排序");
/**
* 如果想降序排序,则可以使用加reversed()
*/
userInfoList = userInfoList.stream().sorted(Comparator.comparing(UserInfo::getAge).reversed()).collect(Collectors.toList());
userInfoList.forEach(a -> System.out.println(a.toString()));
//运行结果
UserInfo{userId=1, userName='捡田螺的小男孩', age=18}
UserInfo{userId=3, userName='捡瓶子的小男孩', age=26}
UserInfo{userId=2, userName='程序员田螺', age=27}
开始降序排序
UserInfo{userId=2, userName='程序员田螺', age=27}
UserInfo{userId=3, userName='捡瓶子的小男孩', age=26}
UserInfo{userId=1, userName='捡田螺的小男孩', age=18}
6.distinct去重
distinct
可以去除重复的元素:
List<String> list = Arrays.asList("A", "B", "F", "A", "C");
List<String> temp = list.stream().distinct().collect(Collectors.toList());
temp.forEach(System.out::println);
7. findFirst 返回第一个
findFirst
很多业务场景,我们只需要返回集合的第一个元素即可:
List<String> list = Arrays.asList("A", "B", "F", "A", "C");
list.stream().findFirst().ifPresent(System.out::println);
8. anyMatch是否至少匹配一个元素
anyMatch
检查流是否包含至少一个满足给定谓词的元素。
Stream<String> stream = Stream.of("A", "B", "C", "D");
boolean match = stream.anyMatch(s -> s.contains("C"));
System.out.println(match);
//输出
true
** 1.2. Description **
- 这是短路端子操作。
- 它返回此流的任何元素是否与提供的谓词匹配。
- 如果不需要确定结果,则可能不会对所有元素都评估谓词。 遇到第一个匹配元素时,方法将返回
true
。 - 如果流为空,则返回
false
,并且不对谓词求值。
9. allMatch 匹配所有元素
allMatch
检查流是否所有都满足给定谓词的元素。
Stream<String> stream = Stream.of("A", "B", "C", "D");
boolean match = stream.allMatch(s -> s.contains("C"));
System.out.println(match);
//输出
false
10. map转换
map
方法可以帮我们做元素转换,比如一个元素所有字母转化为大写,又或者把获取一个元素对象的某个属性,demo
如下:
List<String> list = Arrays.asList("jay", "tianluo");
//转化为大写
List<String> upperCaselist = list.stream().map(String::toUpperCase).collect(Collectors.toList());
upperCaselist.forEach(System.out::println);
11. Reduce
Reduce可以合并流的元素,并生成一个值
int sum = Stream.of(1, 2, 3, 4).reduce(0, (a, b) -> a + b);
System.out.println(sum);
12. peek 打印个日志
peek()
方法是一个中间Stream
操作,有时候我们可以使用peek
来打印日志
List<String> result = Stream.of("程序员田螺", "捡田螺的小男孩", "捡瓶子的小男孩")
.filter(a -> a.contains("田螺"))
.peek(a -> System.out.println("关注公众号:" + a)).collect(Collectors.toList());
System.out.println(result);
//运行结果
关注公众号:程序员田螺
关注公众号:捡田螺的小男孩
[程序员田螺, 捡田螺的小男孩]
13. Max,Min最大最小
使用lambda流求最大,最小值,非常方便。
List<UserInfo> userInfoList = new ArrayList<>();
userInfoList.add(new UserInfo(1L, "捡田螺的小男孩", 18));
userInfoList.add(new UserInfo(3L, "捡瓶子的小男孩", 26));
userInfoList.add(new UserInfo(2L, "程序员田螺", 27));
Optional<UserInfo> maxAgeUserInfoOpt = userInfoList.stream().max(Comparator.comparing(UserInfo::getAge));
maxAgeUserInfoOpt.ifPresent(userInfo -> System.out.println("max age user:" + userInfo));
Optional<UserInfo> minAgeUserInfoOpt = userInfoList.stream().min(Comparator.comparing(UserInfo::getAge));
minAgeUserInfoOpt.ifPresent(userInfo -> System.out.println("min age user:" + userInfo));
//运行结果
max age user:UserInfo{userId=2, userName='程序员田螺', age=27}
min age user:UserInfo{userId=1, userName='捡田螺的小男孩', age=18}
14. count统计
一般count()
表示获取流数据元素总数。
List<UserInfo> userInfoList = new ArrayList<>();
userInfoList.add(new UserInfo(1L, "捡田螺的小男孩", 18));
userInfoList.add(new UserInfo(3L, "捡瓶子的小男孩", 26));
userInfoList.add(new UserInfo(2L, "程序员田螺", 27));
long count = userInfoList.stream().filter(user -> user.getAge() > 18).count();
System.out.println("大于18岁的用户:" + count);
//输出
大于18岁的用户:2
15. 常用函数式接口
其实lambda离不开函数式接口,我们来看下JDK8常用的几个函数式接口:
Function<T, R>
(转换型): 接受一个输入参数,返回一个结果Consumer<T>
(消费型): 接收一个输入参数,并且无返回操作Predicate<T>
(判断型): 接收一个输入参数,并且返回布尔值结果Supplier<T>
(供给型): 无参数,返回结果
Function<T, R>
是一个功能转换型的接口,可以把将一种类型的数据转化为另外一种类型的数据
private void testFunction() {
//获取每个字符串的长度,并且返回
Function<String, Integer> function = String::length;
Stream<String> stream = Stream.of("程序员田螺", "捡田螺的小男孩", "捡瓶子的小男孩");
Stream<Integer> resultStream = stream.map(function);
resultStream.forEach(System.out::println);
}
Consumer<T>
是一个消费性接口,通过传入参数,并且无返回的操作
private void testComsumer() {
//获取每个字符串的长度,并且返回
Consumer<String> comsumer = System.out::println;
Stream<String> stream = Stream.of("程序员田螺", "捡田螺的小男孩", "捡瓶子的小男孩");
stream.forEach(comsumer);
}
Predicate<T>
是一个判断型接口,并且返回布尔值结果.
private void testPredicate() {
//获取每个字符串的长度,并且返回
Predicate<Integer> predicate = a -> a > 18;
UserInfo userInfo = new UserInfo(2L, "程序员田螺", 27);
System.out.println(predicate.test(userInfo.getAge()));
}
Supplier<T>
是一个供给型接口,无参数,有返回结果。
private void testSupplier() {
Supplier<Integer> supplier = () -> Integer.valueOf("666");
System.out.println(supplier.get());
}
这几个函数在日常开发中,也是可以灵活应用的,比如我们DAO操作完数据库,是会有个result的整型结果返回。我们就可以用Supplier<T>
来统一判断是否操作成功。如下:
private void saveDb(Supplier<Integer> supplier) {
if (supplier.get() > 0) {
System.out.println("插入数据库成功");
}else{
System.out.println("插入数据库失败");
}
}
@Test
public void add() throws Exception {
Course course=new Course();
course.setCname("java");
course.setUserId(100L);
course.setCstatus("Normal");
saveDb(() -> courseMapper.insert(course));
}