Java基础大纲
Java-基础
Java特性
- 简单的
- 解释的(有解释器也叫
java虚拟机
)(JVM是内存自动管理) - 面向对象的
- 与平台无关的(跨平台可以运行)
- 健壮的(强类型的)
- 多线程的
- 安全的(内存方面、保护重要文件)
- 动态的语言
内存四大块
例子:
1 | int arr = 10; |
常量池
调用一次开辟一个空间
放10、10、10 。。。
堆栈stack
基本类型的数据或对象的
引用
>
所有局部变量(形参、方法内部局部变量、代码块中局部变量)、基本类型的变量、引用类型的变量
放arr、arrs
堆Heap
new 产生的数据对象本身(new 修饰的实例本体)
引用类型变了所引用的对象(数组、普通Java对象)
放new int[3]
静态域
静态变量、静态常量、流对象、持久化对象
放a
Java存储
寄存器
最快;位于处理器内;数量有限;不能直接控制(在代码中无法感知到它);(C、C++允许建议寄存器的分配方式)
堆栈
位于RAM;指针下移分配新内存,上移则释放;存基本数据类型和对象的引用
堆
常用的内存池,位于RAM;存放所有Java对象;与堆栈比:编译器无需知道 data 在堆里的存活时间,导致存储分配和清理更费时间
常量存储
存在代码中;是安全的;有时在嵌入式系统中常量本身会和其他部分分割开,此时,可选存放在ROM(只读存储器)中
非RAM存储
存放于程序之外;不受程序控制,在未运行时亦可存在。
例子:流对象、持久化对象
修饰符顺序
public、protected、private、abstract、static、final、transient、validate、synchronized、native、strictfp
编码集
unicode
是用2个字节来表示utf-8
是用3个字节来表示
Java环境变量配置
进行java
开发,首先要安装jdk
,安装了jdk后还要进行环境变量配置:
- 下载
jdk
(http://java.sun.com/javase/downloads/index.jsp),我下载的版本是:`jdk-6u14-windows-i586.exe` - 安装
jdk-6u14-windows-i586.exe
- 配置环境变量:右击“我的电脑”–》“高级”–》“环境变量”
- 在系统变量里新建
JAVA_HOME
变量,变量值为:C:\Program Files\Java\jdk1.6.0_14
(根据自己的安装路径填写) - 新建
classpath
变量,变量值为.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar
- 在
path
变量(已存在不用新建)提娜几遍了值:%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin
(注意变量值之间用“;”隔开)
- 在系统变量里新建
- “开始”–》“运行”–》输入“javac”–》“Enter”,如果能正常打印用法说明配置成功!
JAVA_HOME
:jdk
的安装路径classpath
:java
加载类路径,只有类在classpath
中java
命令才能识别,在路径前多加了个“.
”表示当前路径path
:系统在任何路径下都可以识别java
,javac
命令
三种结构:顺序、分支、循环
分支
- if对于布尔型或布尔型表达式进行匹配
- switch对于整型值或字符串进行匹配
if:
- 每一种格式都是单条语句
- 第二种格式和三元运算符的区别:三元运算符一般用于两个数比较大小,必须返回一个值。if可以用于别的语句中
switch 关键字: case取值1“:” break; default:(不一定适用类似于else) switch的执行效率高于if-else
注释
- 单行注释
//
- 多行注释
/* */
- 文档注释:用到javadoc.jexe
/** */
按值传递、按引用传递
- “在Java里面参数传递都是按值传递”这句话的意思是:按值传递是传递值得拷贝,按引用传递传递的是引用的地址值,所以统称按值传递。
- 在Java里只有基本类型和按照下面这种定义方式的
String
(String str = "按值传递"
)是按值传递,其他的都是按引用传递。
- 按值传递就是当参数类型为基本数据类型是不能对值进行改变
- 按引用传递就是当参数类型为引用数据类型时可以对值进行改变
类
英雄攻击怪物
一个类传值给另一个类,用方法传参
1 | system.exit(0);//整个程序结束 |
成员变量
变量处在类中,任何方法外部,就是成员变量
成员变量==全局变量
方法变量==局部变量
方法调用
方法调用不到的原因:没有保存、局部变量、访问权限
调用出问题:有无返回值、
方法调用的时候,因为知道了参数的类型,所以不用在定义类型
普通方法需要方法调用,“类名.方法名”1
2abstract:抽象类
synchronized:同步(线程中用到)方法返回值、参数列表
方法返回值可以是基本数据类型也可以是引用数据类型(包括对象)
方法参数列表可以是基本数据类型也可以是引用数据类型(包括对象),参数可以不止一个return
返回的是一个确定的值,不是表达式:return hp>=0;
形参只是占位的作用
声明的参数一定要确定类型,要弄个类型方法重载
(普通)方法重载:方法名可以一样,但是参数列表的数据类型、参数个数不同即可,参数名称可以相同但尽量不要相同
java中同一个类中允许定义多个同名但不同参数列表的方法(叫做方法重载)
不同参数列表:参数数量个数不同、数据类型不同、参数类型不同
方法最好与类名不同,在需要的时候调用【避免跟构造方法混淆了】Class文件字节码结构组织示意图
注:被编译器编译成的.class字节码文件的字节流以及其组织结构如下所示:
- 魔数:确定这个文件是否为一个能被虚拟机接收的
Class
文件。 - Class 文件版本:Class文件的版本号,保证编译正常执行。
- 常量池:常量池主要存放两大常量:字面量核符号引用。
- 访问标志:标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。
- 当前类索引,父类索引:类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于Java语言的单继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有 java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0;
- 接口索引集合:接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按
implement
(如果这个类本身是接口的话则是extends
)后的接口顺序从左到右排列在接口索引集合中。 - 字段表集合:描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
- 方法表集合:类中的方法。
- 属性表集合:在 Class ⽂件,字段表,方法表中都可以携带⾃⼰的属性表集合。
类加载过程
类加载过程:加载–》连接–》初始化。连接过程又可以分为三步:验证–》准备–》解析
加载过程,主要完成3件事情:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为⽅法区的运⾏时数据结构
- 在内存中⽣成⼀个代表该类的 Class 对象,作为⽅法区这些数据的访问⼊⼝
虚拟机规范上面这3点并不具体,因此是⾮常灵活的。⽐如:”通过全类名获取定义此类的⼆进制字节流” 并没有指明具体从哪⾥获取、怎样获取。⽐如:比较常⻅的就是从 ZIP 包中读取(⽇后出现的JAR、EAR、WAR格式的基础)、其他⽂件⽣成(典型应⽤就是JSP)等等。
⼀个⾮数组类的加载阶段(加载阶段获取类的⼆进制字节流的动作)是可控性最强的阶段,这⼀步我们可以去完成还可以⾃定义类加载器去控制字节流的获取⽅式(重写⼀个类加载器的 loadClass() ⽅法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
类加载器、双亲委派模型也是⾮常重要的知识点,这部分内容会在后⾯的问题中单独介绍到。
加载阶段和连接阶段的部分内容是交叉进⾏的,加载阶段尚未结束,连接阶段可能就已经开始了。
有哪些类加载器?
JVM 中内置了三个重要的 ClassLoader
,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承⾃ java.lang.ClassLoader
:
BootstrapClassLoader
(启动类加载器) :最顶层的加载类,由C++实现,负责加载%JAVA_HOME%/lib
⽬录下的jar包和类或者或被-Xbootclasspath
参数指定的路径中的所有类。ExtensionClassLoader
(扩展类加载器) :主要负责加载⽬录%JRE_HOME%/lib/ext
⽬录下的jar包和类,或被java.ext.dirs
系统变量所指定的路径下的jar包。AppClassLoader
(应⽤程序类加载器) :⾯向我们⽤户的加载器,负责加载当前应⽤classpath下的所有jar包和类。双亲委派模型介绍
每⼀个类都有⼀个对应它的类加载器。系统中的 ClassLoder 在协同⼯作的时候会默认使⽤ 双亲委派模型 。即在类加载的时候,系统会⾸先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,⾸先会把该请求委派给该⽗类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当⽗类加载器⽆法处理时,才由⾃⼰来处理。当⽗类加载器为null时,会使⽤启动类加载器 BootstrapClassLoader 作为⽗类加载器。
每个类都有个父类加载器,通过下面的程序验证输出1
2
3
4
5
6
7
8public static void main(String[] args) {
System.out.println("ClassLodarDemo's ClassLoader is " +
ClassLoaderDemo.class.getClassLoader());
System.out.println("The Parent of ClassLodarDemo's ClassLoader is " +
ClassLoaderDemo.class.getClassLoader().getParent());
System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " +
ClassLoaderDemo.class.getClassLoader().getParent().getParent());
}1
2
3ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@1b6d3586
The GrandParent of ClassLodarDemo's ClassLoader is nullAppClassLoader
的⽗类加载器为ExtClassLoader
。ExtClassLoader
的⽗类加载器为null,null并不代表ExtClassLoader
没有⽗类加载器,⽽是Bootstrap ClassLoader
。
其实这个双亲翻译的容易让别⼈误解,我们⼀般理解的双亲都是⽗⺟,这⾥的双亲更多地表达的是“⽗⺟这⼀辈”的⼈⽽已,并不是说真的有⼀个 Mather ClassLoader 和⼀个 Father ClassLoader 。另外,类加载器之间的“⽗⼦”关系也不是通过继承来体现的,是由“优先级”来决定。官⽅API⽂档对这部分的描述如下:1
2
3
4The Java platform uses a delegation model for loading classes. The basic idea is that
every class loader has a "parent" class loader. When loading a class, a class loader
first "delegates" the search for the class to its parent class loader before attempting
to find the class itself.双亲委派模型实现源码分析
双亲委派模型的实现代码⾮常简单,逻辑⾮常清晰,都集中在java.lang.ClassLoader
的loadClass()
中,相关代码如下所示。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve) throw ClassNotFoundException{
synchronized (getClassLoadingLock(name)){
//首先,检查请求的类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try{
if (parent != null) {//⽗加载器不为空,调⽤⽗加载器loadClass()⽅法处理
c = parent.loadClass(name, false);
} else {//⽗加载器为空,使⽤启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常说明⽗类加载器⽆法完成加载请求
}
if (c == null) {
long t1 = System.nanoTime();
//⾃⼰尝试加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}双亲委派模型带来了什么好处呢?
双亲委派模型保证了Java程序的稳定运⾏,可以避免类的重复加载(JVM 区分不同类的⽅式不仅仅根据类名,相同的类⽂件被不同的类加载器加载产⽣的是两个不同的类),也保证了 Java 的核⼼ API 不被篡改。如果不⽤没有使⽤双亲委派模型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为java.lang.Object
类的话,那么程序运⾏的时候,系统就会出现多个不同的 Object 类。
如果我们不想⽤双亲委派模型怎么办?
为了避免双亲委托机制,我们可以⾃⼰定义⼀个类加载器,然后重载loadClass()
即可。如何⾃定义类加载器?
除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承⾃java.lang.ClassLoader
。如果我们要⾃定义⾃⼰的类加载器,很明显需要继承ClassLoader
。
构造方法
构造方法概念:用于初始化对象实例的一组指令集,不是方法。也称构造器、构造函数
构造方法作用:目的是类对象初始化,但其不是方法,因为它没有返回值,也不能被继承。
构造方法特点
一、方法名必须与类名相同
二、构造方法没有返回值,且不用void修饰
三、构造方法在实例化对象时,由系统自动调用并且强制执行
四、构造方法不能被继承
五、若当前没有声明任何的构造方法,系统(Java虚拟机JVM)会提供一个默认的构造方法,默认的构造方法,不带任何参数,且没有任何操作
构造方法重载
名字可以相同,但是调用的时候看调用的是哪一个(参数列表),与普通方法一样不考虑名字,只考虑参数
判断两个同名的构造方法参数是否相同的时候是根据参数个数以及参数的类型来判断的,不考虑参数的名字
注意
构造方法不能被final(终态)、static(静态)、abstract(抽象)、synchronized(同步)修饰
类文件结构
1 | ClassFile { |
assert 断言
- 语法1:
assert <boolean表达式>
如果<boolean表达式>
为true,则程序继续执行;
如果<boolean表达式>
为false,则程序抛出AssertionError,并终止执行。 - 语法2:
assert <boolean表达式> : <错误信息表达式>
如果<boolean表达式>
为true,则程序继续执行;
如果<boolean表达式>
为false,则程序抛出AssertionError,并输入<错误信息表达式>
%的底层计算
1 | //模拟取余(向0方向舍入)计算,dividend被除数,divisor除数 |
Stream(Java8)
lambda(匿名函数;可表示闭包)
语法格式:
1 | (parameters) -> expression |
lambda表达式的重要特征:
可选类型声明: 不需要声明参数类型,编译器可以统一识别参数值。
可选的参数圆括号: 一个参数无需定义圆括号,但多个参数需要定义圆括号。
可选的大括号: 如果主题包含了一个语句,就不需要使用大括号。
可选的返回关键字: 如果主题只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。
方法引用
方法引用的出现,使得我们可以将一个方法赋值给一个变量或者作为参数传递给另外一个方法。::
双冒号作为方法引用符号。
例子1:
1 | //函数式的接口定义(其内只有一个方法);其实际方法的定义; |
@FuctionalInterface 只能有单个方法
例子 2:
1 | //定义 |
例子 3:
1 | //定义 |
例子 4:
1 | //定义 |
Stream API(常用)
- 链式调用
- Stream接口的定义,继承BaseStream,几乎所有的接口声明都是接收方法引用类型的参数。
of
可接收一个泛型对象或可变泛型集合,构造一个Stream
对象
例子:
1 | private static void createStream(){ |
empty
创建一个空的Stream
对象
concat
连接两个Stream
,不改变其中任何一个Stream
对象,返回一个新的Stream
对象
例子:
1 | private static void concatStream(){ |
max
一般用于求数字集合中的最大值,或者实体中数字类型的属性比较,拥有最大值的那个实体。它接收一个Comparator<T>
例子:引用方法是Integer::compareTo
1 | private static void max(){ |
1 | //自己定制一个 Comparator |
min
与max
用法一样,求的是最小值
findFirst
获取Stream
中的第一个元素
findAny
获取Stream
中某个元素,如果是串行情况下,一般都会返回第一个元素,并行情况下就不一定了。
count
返回元素的个数
例子:
1 | Stream<String> a = Stream.of("a", "b", "c"); |
peek
建立一个通道,在这个通道中对Stream
的每个元素执行对应的操作。对应Comsumer<T>
的函数式接口,这是一个消费者函数式接口,用来消费Stream
元素的。
例子:
1 | //把每个元素转换成对应的大写字母并输出 |
forEach
和peek
方法类似,都接收一个小肥猪函数式接口,对每个元素进行操作。但和peek
不同的是,forEach
执行之后这个Stream
就真的被消费掉了,不可能对它进行后续操作了。
例子:
1 | private static void forEach(){ |
forEachOrdered
功能和forEach
一样,不同的是,forEachOrdered
是有顺序保证的。对Stream
中的元素按插入时的顺序进行消费。(开启并行的时候就有作用了)
例子:
1 | Stream<String> a = Stream.of("a", "b", "c"); |
limit
获取前n条数据,类似于MySQL的limit,只不过只能接收一个参数,就是数据条数
例子:
1 | private static void limit(){ |
skip
跳过前面n条数据
例子:
1 | private static void skip(){ |
distinct
元素去重
例子:
1 | private static void distinct(){ |
sorted
有两个重载,一个无参(默认自然排序),另一个有个Comparator
类型的参数。
例子:
1 | private static void sorted(){ |
为下面的例子方便,模拟了个数据源
1 | private static List<User> getUserData(){ |
filter
条件过滤器
1 | //过滤年龄大于50的 |
map操作
map
接受一个Function
函数式接口,把它翻译成映射最合适了,通过原始数据元素,映射出新的类型
1 | //接口定义 |
例子:
1 | //将DAO实体类型转换成DTO实体类型 |
mapToInt
将元素转换成 int 类型,在 map
方法的基础上进行封装。
mapToLong
将元素转换成 Long 类型,在 map
方法的基础上进行封装。
mapToDouble
将元素转换成 Double 类型,在 map
方法的基础上进行封装。
flatMap
用在些特别的场景下,如Stream
是以下几种结构时,用于将原有二维结构扁平化。
Stream<String[]>
Stream<Set<String>>
Stream<List<String>>
用flatMap
将结果转化为Stream<String>
形式
例子:
1 | private static void flatMap(){ |
flatMapToInt
用法参考 flatMap
,将元素扁平为 int 类型,在 flatMap
方法的基础上进行封装。
flatMapToLong
用法参考 flatMap
,将元素扁平为 Long 类型,在 flatMap
方法的基础上进行封装。
flatMapToDouble
用法参考 flatMap
,将元素扁平为 Double 类型,在 flatMap
方法的基础上进行封装。
collection
在进行了一系列操作后,我们把Stream
类型的结果转换成List
、Map
这样的常用的数据结构,collection
可以实现转换的目的。
例子:
1 | //定义 collection 接口 |
Collectors
已提供了很多可拿来即用的收集器
Collectors.toList()
、Collectors.toSet()
、Collectors.toMap()
、Collectors.groupingBy()
用来分组
例子:
1 | //按照 userId 字段分组,返回以userId为key,List为value的Map,或者返回每个key的个数 |
toArray
collection
返回列表、map等,toArray
返回数组,有两个重载,一个空参数,返回的是Object[]
,另一个接收一个IntFunction<R>
类型参数
1 | //定义接口 |
reduce
作用:每次计算都用上一次的计算结果,比如求和
例子:
1 | private static void reduce(){ |
anyMatch
并行Stream
通过users.parallelStream()
或users.stream().parallel()
的方式来创建并行Stream
对象。
并行Stream
默认使用ForkJoinPool
线程池,也支持自定义。
使用并行Stream的场景
- 多核CPU的前提
- 数据量大的情况
- CPU密集型计算的情况。而IO密集型使用并行
Stream
反而会更慢 - 并行计算可能更快,但大多数时候需要用
collect
合并,若合并代价很大,不适合用并行Stream
- 有些操作,如
limit
,findFirst
、forEachOrdered
等依赖于元素顺序的操作,不适合用并行Stream
CopyOnWriteArrayList在多线程的时候用到
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。
读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。
static
- static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。
- 静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。
- 能通过this访问静态成员变量吗?
所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。 - static是不允许用来修饰局部变量
final
- 可以声明成员变量、方法、类以及本地变量
- final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误
- final变量是只读的
- final申明的方法不可以被子类的方法重写
- final类通常功能是完整的,不能被继承
- final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销
- final关键字提高了性能,JVM和Java应用都会缓存final变量,会对方法、变量及类进行优化
- 方法的内部类访问方法中的局部变量,但必须用final修饰才能访问
valatile
当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步,因此在读取volatile类型的变量时总会返回最新写入的值。
当一个变量定义为 volatile 之后,将具备以下特性:
- 保证此变量对所有的线程的可见性,不能保证它具有原子性(可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的)
- 禁止指令重排序优化
- volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行
AtomicInteger 中主要实现了整型的原子操作,防止并发情况下出现异常结果,其内部主要依靠JDK 中的unsafe 类操作内存中的数据来实现的。volatile 修饰符保证了value在内存中其他线程可以看到其值得改变。CAS操作保证了AtomicInteger 可以安全的修改value 的值。
synchronized
当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。
Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。
根据获取的锁分类
获取对象锁
- synchronized(this|object) {}
- 修饰非静态方法
获取类锁
- synchronized(类.class) {}
- 修饰静态方法
原理
同步代码块:
- monitorenter和monitorexit指令实现的
同步方法
- 方法修饰符上的ACC_SYNCHRONIZED实现
Lock
悲观锁、乐观锁
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现。乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
自旋锁、适应性自旋锁
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
死锁
当前线程拥有其他线程需要的资源,当前线程等待其他线程已拥有的资源,都不放弃自己拥有的资源。
import
单类型导入和import *
按需类型导入
结论:推荐使用单类型导入
分析:
单类型导入(如:import java.io.File;
)
按需类型导入(如:import java.io.*
)
按需类型导入一整个包下所有类,而仅仅导入当前需要使用的类。
但如果一个有多个按需类型导入的情况下会列出所有可能的情况,如:
1 | package com; |
引用到File类的话,则会列出所有可能的情况
1 | File //File类属于无名包,就是说File类没有package语句,编译器会首先搜索无名包 |
会查找出所有的可能情况以确定是否有类导入冲突。
假设此时的顶层路径有三个,那么编译器就会进行3*5=15次查找
如果查找玩编译器发现了两个同名的类,那么就会报包冲突的错误。
按需类型导入不会降低Java代码的执行效率,但会影响到Java代码的编译速度。
命名规则
- 以字母、下划线、美元符开头(不能以数字开头);
- 后面跟字母、下划线、美元符或数字;
- 命名没有长度限制;
- 对大小写敏感;
- 不可以用已有的关键字
常量:字面常量(6种)和有名常量final
字面常量: 整数常量(包括负数);小数常量;布尔型常量;字符常量(‘数字字母或符号’);
字符串常量:将一个或者多个字符用双引号标识;空常量null
Java命名规范
避免使用下划线(除静态常量)
驼峰命名法:
类名、接口名:多单词XxxYyyZxx
常量 Xxx_Yyy_Zzz ????
数据类型
基本数据类型
名称 | 类型 | 大小 | 值 | 备注 |
---|---|---|---|---|
boolean | 布尔型 | 1个字节 | true和false | true、false是具体值,不是关键字但也不能用作变量名 |
byte | 字节型 | 1个字节 | -2^7~2^7-1 | |
char | 字符型 | 2个字节 | c语言(1个字符); | |
无符号整数 | 2个字节 | 0~2^16 | ||
short | 短整型 | 2个字节 | -2^15~2^15-1 | |
int | 整型 | 4个字节 | -2^31~2^31-1 | |
long | 长整型 | 8个字节 | -2^56~2^56-1 | long的取值范围比float的小(浮点数用科学计数法表示) |
float | 浮点型 | 4个字节 | -2^128~2^127 | 最高位(31):0正、1负 前两个字节表示整数部分 后两个字节表示小数部分 符号位(S):1bit;指数位(E):8bit;尾数位(M):23bit |
double | 双精度 | 8个字节 | -2^1024~2^1023 | if(d1 == d2) 这种是错误的,因为精度原因不能直接比较 |
String若是放一个UTF-8的常量串,其长度最长是:65535个字节(不是字符)。String内部是以char数组的形式存储,数组的长度是int类型,那么String允许的最大长度就是Integer.MAX_VALUE,2147483647。又由于java中的字符是16位存储的,因此大概需要4GB的内存才能存储最大长度的字符串。
double的值比较
1 | boolean equal(double num1, double num2){ |
1 | public class Test { |
类型转换
- 自动类型转换【隐式类型转换】(占位数少的类型赋值给占位数多的,如int赋值给long)
- byte型、short型和char的值将被提升到int型
- 如果一个操作数是long型,计算结果就是long型;
- 如果一个操作数是float型,计算结果就是float型;
- 如果一个操作数是double型,计算结果就是double型
- 强制类型转换【显式类型转换】(级别高赋值给级别低的,如double赋值给float)
Integer缓存区
1 | Integer a = 1000,b = 1000; |
解析:
观察valueOf()类函数,看到
1 | public static Integer valueOf(int i) { |
如果值在 -128到127之间,那么就会返回该缓存的实例。
因此 Integer c = 100,d = 100;两者指向同样的对象。
自定义Integer缓存区的值
1 | public static void testCache() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { |
即newCache中把原来newCache[132]的位置的值替换成了newCache[133]就是4所在位置值换成了5。
BigDecimal:使用它来确保精度
1 | BigDecimal bd =new BigDecimal(BaseParser.parseDouble(strEdit) - BaseParser.parseDouble(strEdit) * taxRate); |
字符类型
它使用Unicode
字符集作为它的常量,也就是它有65535
个常量0~65535
引用数据类型
数组(
[]
)、类(class
)和接口(interface
)引用类型还有一种特殊的
null
类型。所谓引用数据类型就是对一个对象的引用,对象包括实例和数组两种。实际上,引用类型变量就是一个指针,只是 Java 语言里不再使用指针这个说法。
空类型(null type)就是 null 值的类型,这种类型没有名称。因为 null 类型没有名称,所以不可能声明一个 null 类型的变量或者转换到 null 类型。
空引用(null)是 null 类型变量唯一的值。空引用(null)可以转换为任何引用类型。
在实际开发中,程序员可以忽略 null 类型,假定 null 只是引用类型的一个特殊直接量。
注意:空引用(null)只能被转换成引用类型,不能转换成基本类型,因此不要把一个 null 值赋给基本数据类型的变量。
对象、父子类互转
类、对象、引用
类 对象/实例/对象的引用 = new 类();对象/实例/对象的引用:指向“new 类()”的内存地址的首地址
new 类():对象/实例,不能叫做对象的引用父类转子类:父类的引用必须指向子类的实例,才能强转成子类
1 | public class Person { |
引用类型强度排序
强引用 > 软引用 > 弱引用
引用类型 | 说明 |
---|---|
StrongReference (强引用) |
当一个对象具有强引用,那么垃圾回收器是绝对不会的回收和销毁它的,非静态内部类会在其整个生命周期中持有对它外部类的强引用 |
WeakReference (弱引用) |
在垃圾回收器运行的时候,如果对一个对象的所有引用都是弱引用的话,该对象会被回收 |
SoftReference (软引用) |
如果一个对象只具有软引用,若内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,才会回收这些对象的内存 |
PhantomReference (虚引用) |
一个只被虚引用持有的对象可能会在任何时候被GC 回收。虚引用对对象的生存周期完全没有影响,也无法通过虚引用来获取对象实例,仅仅能在对象被回收时,得到一个系统通知(只能通过是否被加入到ReferenceQueue 来判断是否被GC ,这也是唯一判断对象是否被GC 的途径)。 |
进制
- java中二进制不显示,0开头的是八进制(比如
07
,范围0~7
),0x开头是十六进制(比如0*c
,范围:0~9ABCDEF
)如:
1 | System.err.println(101 + 011 + 110+"");//结果220 |
- StringBuffer默认的构造器
StringBuffer sb=new StringBuffer();
默认构造器是由系统自动分配容量,而系统容量默认值是16个字符
进制互转
循环
break
:跳出当前的代码块,在for
中直接跳出for
循环continue
:在for
中结束这一次循环继续下一次
循环语句一般包含4个部分:
- 初始化语句(在while外面)
- 循环条件
- 循环体
- 迭代语句(num++)
do-while
(do{ }while(条件)
;) while
for
(初始化语句只执行一次)
双层for
中(里面的for
前面加了标号
,break 标号
;)那么会直接跳出外面那个for循环
标号的出现可以调到指定的语句 标号
一般能明确循环次数的话就用for,否则用while
面向对象(自顶向下)
面向过程(Procedure)–> 面向对象(Object) –> 面向组件(Component) –> 面向服务(Service) –> Saas/PasS/IasS –> 互联网系统
程序=算法+数据结构
可以简单重构成:
程序=基于对象操作的算法+以对象为最小单位的数据结构
面向对象的本质就是让对象有多态性,把不同对象以同一特性来归组,统一处理。至于所谓继承父类、实现接口等概念,只是多态性的实现细节有不同。
运算符
算术运算符
%取余(取模) 取余等于%=:i%=3相当于i=i%3; !=不等于 >=大于等于
逻辑运算符
!非 &&与 ||或
位运算符(二进制)
1 |
|
1 | System.out.println("java 右移"); |
三元运算符
(条件表达式)?表达式1:表达式2;
运算符的优先级(从高到低)
优先级 | 描述 | 运算符 |
---|---|---|
1 | 括号 | ()、[] |
2 | 正负号 | +、- |
3 | 自增自减,非 | ++、–、! |
4 | 乘除,取余 | * 、/、% |
5 | 加减 | +、- |
6 | 移位运算 | <<、>>、>>> |
7 | 大小关系 | >、>=、<、<= |
8 | 相等关系 | ==、!= |
9 | 按位与 | & |
10 | 按位异或 | ^ |
11 | 按位或 | ` |
12 | 逻辑与 | && |
13 | 逻辑或 | ` |
14 | 条件运算 | ?: |
15 | 赋值运算 | =、+=、-=、*= 、/=、%= |
16 | 位赋值运算 | &=、 |
如果在程序中,要改变运算顺序,可以使用()。
集合
集合:动态的对象数组
List接口存储一组不唯一,有序(插入顺序)的对象
Set 接口存储一组唯一,无序的对象。
HashMap
是非synchronized
的,性能更好,HashMap
可以接受**为null
的key
和value
**,Hashtable
是线程安全的,比HashMap
要慢,不接受null
进制
java
中二进制不显示,0
开头的是八进制,0x
开头是十六进制如:1
System.err.println(101 + 011 + 110+"");//结果220
StringBuffer
默认的构造器StringBuffer sb=new StringBuffer();
默认构造器是由系统自动分配容量,而系统容量默认值是16
个字符
进制互转
静态
静态static
用来修饰成员变量和成员方法,也可以形成static代码块
被静态static修饰的成员变量和成员方法 独立于该类的任何对象
static对象可以在它的任何对象创建之前访问,无需引用任何对象
静态变量在内存中只有一个拷贝
实例变量可以在内存中有多个拷贝,互不影响
static用处:
在对象之间共享值时
访问与对象无关的变量时
静态方法中无法访问属于实例的实例变量和非静态方法,只可以访问属于类的静态变量和静态方法
静态变量
被static修饰的成员变量和成员方法独立于该类的任何对象
static对象无需引用任何对象就可以直接被调用
在类成员变量中,被static修饰的变量叫做静态变量或类变量
不被static修饰的变量叫做实例变量
static只用于修饰成员变量,类
用到static的地方:在对象之间共享值时、访问与对象无关的变量时
静态常量
静态常量,有static和final修饰:public static final PI 3.1415926
有final修饰的必然是常量,且在定义的时候要进行初始化
静态常量与静态变量一样可以通过类调用,
静态方法
静态方法中只能直接访问静态变量或静态方法
如果对象只用一次的话,用匿名对象,用过一次就成垃圾等待垃圾回收。如:new Demo().getAge();
静态方法不能操作对象,只能直接访问静态方法和静态变量,不能访问实例
静态代码块
三大特性:封装、继承、多态
封装
继承
实例化子类对象,如果子类构造方法中没有调用父类的构造方法,那么默认会调用父类的无参构造方法。
子类如果不用父类中的方法那么要重写方法,如果要调用父类的方法,那么可以不用写父类的方法
抽象类和接口的区别
共同点
抽象类和接口都不能生成具体的实例
都是作为上层使用
不同点
抽象类可以有属性和成员方法,接口不可以
单继承,多实现
抽象类中的变量是普通变量,接口中的变量是静态变量
抽象类表达的是一种is-a的关系,即父类和派生子类在概念上的本质是相同的。
接口表达的是一种like-a的关系,即接口和实现类的关系只是实现了定义行为,并无本质上的联系。
多态
多态的定义
“相同操作,不同结果”(相同信息,送给不同类型的对象,会有不同结果)
允许不同类对同一消息做出响应。
多态存在的条件:
- 要有继承
要有复写override
父类引用指向子类对象
多态分为
编译时和运行时
Java中多态的实现方式:接口实现,继承父类进行方法重写,同一个类中的方法重载
创建子类对象时,调用的方法为子类重写的方法或者继承的方法
如果我们在子类中编写一个独有的方法,此时就不能通过父类的引用创建的子类对象来调用该方法
多态体现在继承中
向上转型:
1 | ClassA a=new ClassB(); |
向下转型:(不用的)
1 | //编译时,b1是父类型 |
- 子类转成父类的规则:自动进行类型转换;父类型 变量名=new 子类型();
- 父类转成子类的规则:强制类型转换;强转后才能访问子类特有的方法
同名不同参
参数个数、类型、顺序的不同
注意:方法返回类型不一致不行
抽象和接口
- 抽象类不能有对象(不能用new此关键字来创建抽象类的对象)
- 抽象类中的抽象方法必须在子类中被重写
- 接口中的所有属性默认为:public static final ;
- 接口中的所有方法默认为:public abstract ;
内部类
内部类的概念
匿名内部类要调用方法内的变量,那么此变量为final型的
内部类:定义在其他类内部的类
创建(非静态)内部类对象:
通过外部类对象来创建(非静态)内部类对象
1 | OuterClass outer=new OuterClass(); |
或者:
1 | OuterClass.InnerClass inner= new OuterClass().new InnerClass();//匿名形式。跟对象有关 |
创建(静态)内部类对象
1 | OuterClass.StaticInnerClass staticinner= new OuterClass.StaticInnerClass(); |
注意
内部类不会创建一个外部类一样的空间,只会创建一个内部类空间,
内部类中变量访问遵循“就近原则”
内部类可以实现“多重继承”,即外部类继承,内部类写的时候没写继承但也有继承
内部类分为:成员内部类(分为静态内部类和非静态内部类)、局部内部类
一个类调用另一个类的方法要创建对象,再通过对象调用方法。但是内部类用外部类的东西可以直接用。
- 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
- 内部类的方法可以直接访问外部类的所有数据,包括私有的数据。
内部类种类
非静态内部类:
非静态内部类会持有一个外部类的实例(在非静态内部类断点会看到
this$0
这个就是外部类的实例)
静态内部类:
也称类内部类。是外部类相关的,属于整个外部类,而不是单纯的属于外部类的某个对象
静态方法只能直接访问静态变量或静态方法
静态内部类不能直接访问外部类的非静态变量
局部内部类:
(作用范围是这个方法内)(没用到)调用方法与调用普通方法一样,要用内部类创建一个对象,再用这个对象调用这个局部内部类
匿名内部类:
必须继承一个父类或实现一个接口。适用仅创建使用一次的类。
匿名内部类形式:参数式(匿名内部类整体当作参数);继承式(抽象类中,实现时当子类继承);接口式(与继承式写法一样)。
可变参数(参数个数不确定包括零个):
类型确定的情况,java把可变参数当作数组来处理
如:
1 | public static void changeParms(int... x){ |
如果参数列表中还有其他的参数类型,那么可变参数必须放在最后面
外部类是静态的,要调用内部类的方法:
1、如果内部类是静态方法:外部类名.内部类名 对象=new 外部类名.内部类名;
2、如果内部类是普通方法:外部类名.内部类名 对象=new 外部类名().new 内部类名;
线程
线程的几个状态
线程和进程
- 支持多进程的操作系统不一定支持多线程。因为线程切换率比较高
- 父进程和子进程有各自独立的数据空间和代码;线程不能独立运行,父线程和子线程共享相同的数据空间并共享系统资源
线程生命周期
线程五个状态:新建状态(被产生时状态)、就绪(start())、运行、阻塞(I/O阻塞或处于挂起)、消亡(代码全部运行完毕、循环进不去、调用stop()方法)
线程实现方式
创建新执行线程两种方法:
1、继承Thread类,创建该类的子类来实现多线程
2、通过实现一个接口,然后实现run方法,再创建Thread
线程基本控制方法
sleep()
休眠状态的时候线程还抓着cpu但是抓的力度会比较弱,cpu容易被抢走
数据共享
Runnable中多个线程共同操作一个对象(在内存中只有一个数据),才会出现“数据共享”的情况或者多个对象,但操作同一个static数据
extends继承Thread类不能起到共享资源的效果,而implents Runnable能够起到共享cpu资源的目的
优先级设置
数值越大说明抓cpu的能力越强,抓到的几率越大
设置守护线程或用户线程(即后台线程)
(守护主线程)只要主线程一结束,守护线程也马上结束
cpu会为每个进程分配一个端口,这样每个进程就有各自的一个cpu区域。而同一个进程中的线程可以参加抢夺属于这个进程的cpu资源,
同时如果加了Thread.sleep(100L);
就能让当前运行的线程休眠一段时间,即让剩下的线程继续抢cpu资源。
线程1抢到数据,进入休眠,线程3抢到这个cpu资源(还没变)进入休眠,线程2抢到资源(这个资源可能变化)。
输出数据时线程可能抢到资源却没来得及输出就又去抢新的资源,那么可能出现某个资源不输出
join(加入执行)
强制停止当前运行的线程,直到join方法执行完才会执行。join还可以设置执行时间
若join在主线程中调用,那么主线程停止,join子线程继续执行
联网和时间比较长的都放到线程中
休眠与等待的区别
休眠时线程还抓着cpu,但是抓的力度比较弱;但是等待时则放开cpu
线程安全
同步代码块:在参数列表中放置锁对象
作用:只有同步代码块中的执行完了,才会线程切换。这样就保证了线程的安全
线程同步
普通同步方法:锁对象是this
静态同步方法:锁对象是类名.class—>字节码对象
同步代码块中:锁对象可以是this,也可以是object
线程死锁
死锁发生时:(同步嵌套同步)
同步代码块中调用同步方法
同步方法中调用同步代码块
常用类
system类
常用数据类型的封装类
system类包含三个成员属性
- 标准输入流(in)
- 标准输出流(out)
- 标准错误输出流(err)
封装类特点:java可以直接处理基本类型的,但是有些情况下需要将其作为对象来处理,
这时需要将其转化为包装类了。
string类
是被final修饰的类不能被继承
1 | String s=".....";//直接赋值 |
字符串比较
a.equals(b)
:比较两个字符串内容的大小,返回值boolean型a==b
:比较的是两个字符串的对象(地址)是否一致,返回值boolean型。如果是new出来的不同对象,那么他们的地址不一样
字符串连接
“+”或者concat(String str)方法(不过后者会产生垃圾不提倡)
1
2
3
4String name="abc";
String name1=name.concat(" ");//括号里必须是String型数据
String name2=name1+123;//“+”号左右只要有一个字符串,那么另一个也会转成字符串类型
//“+”加号里面有了append的连接作用,所以final型的name2才可以改变1
String name2="we"+"are"+"pig";//旧版本中在内存中是五个对象,新版本中是一个对象
append连接
1 | System.out.pringtln(name3.indexOf("d"));//找第一次出现的d在连接后的字符串中的索引,括号内容是char型也可以是可以表示字符(ASCL)的数字,若找不到则返回-1; |
1 | System.out.pringtln(name3.lastindexOf("d"));//找最后一个d出现的索引 |
字符串截取
1 | system.out.pringtln(name3.substring(5,7));//截取第5到第7个字符,包含了第5个字符,但不包含第7个字符。 |
1 | system.out.pringtln(name3.substring(5));//截取从指定位置开始到结束 |
字节数组转化为字符串
1 | byte[] b=new name3.getString |
int型转成字符串
字符串转成int型
字符串拆分
1 | String[] split(String regex);String[] split(String regex,int limit);//limit表示拆成两段,如果数字大于本来可拆分的最大段数,那么取最大段数 |
字符串长度
1 | name3.length();//字符串长度获取要用方法获取 |
1 | strSplit.length;//数组通过属性来获取 |
StringBuffer线程安全的可变字符序列。类似于String的字符串缓冲区,但不能修改。
StringBuider与StringBuffer的API共享
输出中如果加了字符串,那么默认是字符串型数据,会把其余数转成字符串型。
若想要输出别的类型的那么不加字符串
String的源码实现
String、StringBuffer、StringBuilder
- String是final类,不能被继承。对于已经存在的String对象,修改它的值,就是重新创建一个对象
- StringBuffer是一个类似于String的字符串缓冲区,使用append()方法修改Stringbuffer的值,使用toString()方法转换为字符串,是线程安全的
- StringBuilder用来替代于StringBuffer,StringBuilder是非线程安全的,速度更快
==问题
字符串常量会被放在方法区
Object的equals与String的equals
1 | Object a = null; System.out.println(a.equals("ssss"));//报空指针错误System.out.println("ssss".equals(null));//不报错 |
原因:
例1的equals是Object的equals,例2的equals是String的equals
1 //Object的equals源码public boolean equals(Object obj) {return (this == obj);}
1 //String的equals源码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;}
Object
方法:hashcode
/toString
/equal
/clone
/finalize
/线程的一些方法
equals 方法
对两个对象的地址值进行的比较(即比较引用是否相同)
1 | public boolean equals(Object obj) { return (this == obj);} |
equal和==区别
==
基础类型:值
引用类型:地址
equal
默认地址
可根据业务修改(如String重写了equal)
equal和
hashcode
关系
hashCode 方法
hashCode() 方法给对象返回一个hash code值。这个方法被用于 hash tables,例如HashMap。
它的性质是:
在一个Java应用的执行期间,如果一个对象提供给 equals 做比较的信息没有被修改的话,该对象多次调用hashCode() 方法,该方法必须始终如一返回同一个 integer。
如果两个对象根据 equals(Object) 方法是相等的,那么调用二者各自的 hashCode() 方法必须产生同一个 integer 结果。
并不要求根据 equals(Object) 方法不相等的两个对象,调用二者各自的 hashCode() 方法必须产生不同的 integer 结果。然而,程序员应该意识到对于不同的对象产生不同的 integer 结果,有可能会提高 hash table 的性能。
在 JDK 中,Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法直接返回对象的 内存地址。在 String 类,重写了 hashCode 方法
1 | public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h;} |
Object.hashCode()方法与System.identityHashCode(object)的区别
1 | String a = new String("hhh");String b = new String("hhh");System.out.println(System.identityHashCode(a));System.out.println(System.identityHashCode(b));System.out.println(a.hashCode());System.out.println(b.hashCode()); |
打印结果:
前两个不同
后两个相同
分析:
这是为什么呢,我们知道目前a和b是两个不同的对象,他们在内存中存放的地址是不同的,System.identityHashCode方法是java根据对象在内存中的地址算出来的一个数值,不同的地址算出来的结果是不一样的。因此这里打印出的结果不一样。
doc上如是说:返回给定对象的哈希码,该代码与默认的方法 hashCode() 返回的代码一样,无论给定对象的类是否重写 hashCode()。
但是为什么后两个相同呢?这是因为,String类中已经重新写了hashCode()方法,也就是说,String类中hashcode,已经不是根据对象在内存中的地址计算出来的。(具体怎么算出来的,我还没有研究),就是说即使对象在内存中的地址不一样,String中hashcode也可能一样
Math类
Random类
冒泡排序
时间类
Date类表特定的瞬间,精确到毫秒值示
Date() 精确到毫秒值 选择java的util的包 CST:中国标准时间
Date(long date)精确到指定的毫秒值
SimpleDateFormat
SimpleDateFormat 是一个以与语言环境有关的方式来格式化和解析日期的具体类。
它允许进行格式化(日期 -> 文本)、解析(文本 -> 日期)和规范化。
按时间模板解析(解析模板要对应时间),放在date类中;格式化(用第二个模板)
Calender类:
异常处理
- Exception、Error是Throwable类的子类
- Error类对象由Java虚拟机生成并抛出,不可捕捉
- 不管有没有异常,finally中的代码都会执行
- 当try、catch中有return时,finally中的代码依然会继续执行
常见的Error
常见的Error | ||
---|---|---|
OutOfMemoryError | StackOverflowError | NoClassDeffoundError |
常见的Exception
常见的Exception | ||
---|---|---|
常见的非检查性异常 | ||
ArithmeticException | ArrayIndexOutOfBoundsException | ClassCastException |
IllegalArgumentException | IndexOutOfBoundsException | NullPointerException |
NumberFormatException | SecurityException | UnsupportedOperationException |
常见的检查性异常 | ||
IOException | CloneNotSupportedException | IllegalAccessException |
NoSuchFieldException | NoSuchMethodException | FileNotFoundException |
try、catch、finally中的细节分析
看一个例子(例1),来讲解java里面中try、catch、finally的处理流程
1 | public class TryCatchFinally { |
首先程序执行try语句块,把变量t赋值为try,由于没有发现异常,接下来执行finally语句块,把变量t赋值为finally,然后return t,则t的值是finally,最后t的值就是finally,程序结果应该显示finally,但是实际结果为try。为什么会这样,我们不妨先看看这段代码编译出来的class对应的字节码,看虚拟机内部是如何执行的。
我们用
1 | javap -verbose TryCatchFinally |
来显示目标文件(.class文件)字节码信息
系统运行环境:mac os lion系统 64bit
jdk信息:Java(TM) SE Runtime Environment (build 1.6.0_29-b11-402-11M3527) Java HotSpot(TM) 64-Bit Server VM (build 20.4-b02-402, mixed mode)
编译出来的字节码部分信息,我们只看test方法,其他的先忽略掉
1 | public static final java.lang.String test(); |
首先看LocalVariableTable信息,这里面定义了两个变量 一个是t String类型,一个是e Exception 类型接下来看Code部分
第[0-2]行,给第0个变量赋值“”,也就是String t=””;
第[3-6]行,也就是执行try语句块 赋值语句 ,也就是 t = “try”;
第7行,重点是第7行,把第s对应的值”try”赋给第三个变量,但是这里面第三个变量并没有定义,这个比较奇怪
第[8-10] 行,对第0个变量进行赋值操作,也就是t=”finally”
第[11-12]行,把第三个变量对应的值返回
通过字节码,我们发现,在try语句的return块中,return 返回的引用变量(t 是引用类型)并不是try语句外定义的引用变量t,而是系统重新定义了一个局部引用t’,这个引用指向了引用t对应的值,也就是try ,即使在finally语句中把引用t指向了值finally,因为return的返回引用已经不是t ,所以引用t的对应的值和try语句中的返回值无关了。
下面再看一个例子:(例2)
1 | public class TryCatchFinally { |
这里稍微修改了 第一段代码,只是在finally语句块里面加入了 一个 return t 的表达式。按照第一段代码的解释,先进行try{}语句,然后在return之前把当前的t的值try保存到一个变量t’,然后执行finally语句块,修改了变量t的值,在返回变量t。这里面有两个return语句,但是程序到底返回的是try 还是 finally。接下来我们还是看字节码信息
1 | public static final java.lang.String test(); |
这段代码翻译出来的字节码和第一段代码完全不同,还是继续看code属性
第[0-2]行、[3-5]行第一段代码逻辑类似,就是初始化t,把try中的t进行赋值try
第6行,这里面跳转到第17行,[17-19]就是执行finally里面的赋值语句,把变量t赋值为finally,然后返回t对应的值
我们发现try语句中的return语句给忽略。可能jvm认为一个方法里面有两个return语句并没有太大的意义,所以try中的return语句给忽略了,直接起作用的是finally中的return语句,所以这次返回的是finally。
接下来再看看复杂一点的例子:(例3)
1 | public class TryCatchFinally { |
这里面try语句里面会抛出 java.lang.NumberFormatException,所以程序会先执行catch语句中的逻辑,t赋值为catch,在执行return之前,会把返回值保存到一个临时变量里面t ‘,执行finally的逻辑,t赋值为finally,但是返回值和t’,所以变量t的值和返回值已经没有关系了,返回的是catch
例4:
1 | public class TryCatchFinally { |
这个和例2有点类似,由于try语句里面抛出异常,程序转入catch语句块,catch语句在执行return语句之前执行finally,而finally语句有return,则直接执行finally的语句值,返回finally
例5:
1 | public class TryCatchFinally { |
这个例子在catch语句块添加了Integer.parser(null)语句,强制抛出了一个异常。然后finally语句块里面没有return语句。继续分析一下,由于try语句抛出异常,程序进入catch语句块,catch语句块又抛出一个异常,说明catch语句要退出,则执行finally语句块,对t进行赋值。然后catch语句块里面抛出异常。结果是抛出java.lang.NumberFormatException异常
例子6:
1 | public class TryCatchFinally { |
这个例子和上面例子中唯一不同的是,这个例子里面finally 语句里面有return语句块。try catch中运行的逻辑和上面例子一样,当catch语句块里面抛出异常之后,进入finally语句快,然后返回t。则程序忽略catch语句块里面抛出的异常信息,直接返回t对应的值 也就是finally。方法不会抛出异常
例子7:
1 | public class TryCatchFinally { |
这个例子里面catch语句里面catch的是NPE异常,而不是java.lang.NumberFormatException异常,所以不会进入catch语句块,直接进入finally语句块,finally对s赋值之后,由try语句抛出java.lang.NumberFormatException异常。
例子8:
1 | public class TryCatchFinally { |
和上面的例子中try catch的逻辑相同,try语句执行完成执行finally语句,finally赋值s 并且返回s ,最后程序结果返回finally
例子9:
1 | public class TryCatchFinally { |
这个例子中,对finally语句中添加了String.valueOf(null), 强制抛出NPE异常。首先程序执行try语句,在返回执行,执行finally语句块,finally语句抛出NPE异常,整个结果返回NPE异常。
操作流
转换流
OutputStreamWriter
:可以将输出的字符流变为字节流的输出形式InputStreamReader
:将输入的字节流变为字符流输入形式
写入数据:
程序–>字符数据–>字符流–>
OutputStreamWriter
–>字节流–>文件
程序–>–>文件
读取数据:
程序<–字符数据<–字符流<–
InputStreamRreader
<–字节流<–文件
只要用到缓冲区就一定要刷新
内存操作流
随机访问流RandomAccessFile类
数据操作流
常用编码集
类中只要有toString若没有写toString方法,那么会默认调用toString方法
IO流
掌握file文件类
File类的实例不可变,一旦创建,表示的路径不可改变
作业:通过递归算法,创建两个文件夹且每个文件夹中都有各自的文件
IO流步骤
- 找到一个要操作的资源,可能是文件,可能是其他的位置
- 根据字节流或字符流的子类,决定输入及输出的位置
- 进行读或写的操作
- 关闭
- 有Bu开头的是有缓冲区的流
- 以Stram结尾的是字节流
- 以reader结尾的是字符流
InputStreamReader()
//字符流转成字节流的桥梁
字节流:程序–》字节流–》操作文件
字符流:程序–》字节流–》缓存–》操作文件
纯文本(本身就是字符)的时候用字符流比较方便
图片、音频文件的时候(不能转成字符)用字节流
即,都用字节流操作都行
输入输出流的缓冲区默认大小是8K,即8192bite
流的规律
流操作的基本规律:
最痛苦的就是流对象有很多,不知道改用哪一个
通过三个明确来完成
明确源和目的
源:输入流。
InputStream
Reader
目的:输出流。
OutputTream
Writer
操作的数据是否是纯文本
是:字符流
不是:字节流
当体系明确后,再明确要使用哪个具体的对象
通过设备来进行区分:
源设备:内存,硬盘,键盘
目的设备:内存,硬盘,控制台
例子
需求:将一个文本文件中数据存储到另一个文件中。复制文件。
分析:
1 | 源:因为是源,所以使用读取流。InputStream Reader |
1 | 目的:OutputTream Writer |
需求:将键盘录入的数据保存到一个文件中
这个需求中有源和目的都存在
那么分别分析
1 | 源:InputStream Reader |
1 | 目的:OutputTream Writer |
扩展一下,想要把录入的数据按照指定的编码表(utf-8),将数据存到文件中。
但是存储时,需要加入指定编码表。而指定的编码表只有转换流可以指定。
所以要使用的对象是OutputStreamWriter
而该转换流对象要接收一个字节输出流。而且还可以操作的文件的字节输出流FileOutputStream
1 | OutputStreamWriter osw =new OutputStreamWriter(new FileOutputStream(”d.txt”),”UTF-8”); |
需要高效吗?需要。
1 | BufferWriter bufw=new BufferWriter(osw); |
所以记住,转换流怎么使用,字符和字节之间的桥梁,通常,涉及到字符编码转换时,需要用到转换流。(搞码表用的,父类(InputStreamReader
)可以用别的码表,子类(FileReader
)的编码写死了,不能变)
问题
List addall 报java.lang.UnsupportedOperationException
报错代码:
1 | String[] membersArray = request.getParameterValues('members'); |
原因:Not every List
implementation supports the add()
method.
解决:List<String> memberList = new ArrayList<String>(Arrays.asList(membersArray));
Long.parseLong(str)、Integer.parseInt(str)、Double.parseDouble(str)的区别
Long.parseLong(str)
、Integer.parseInt(str)
catch NumberFormatException
就可以了,str
为 null
也可以 catch
住。
Double.parseDouble(str)
当 str
为 null
时会抛出 NPE
,catch NumberFormatException
不行的。
Exception in thread “main” java.lang.UnsupportedClassVersionError: Test has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0
原因:java -version
版本 和 javac -version
版本不一致
处理:
方法1:把环境%JAVA_HOME%\bin
放到第一项
方法2:重新下载 jdk 和 java
方法3:不管它版本不一致,用其他IDE