- 基础语法
- 常用数字类型的区别
- Float在JVM的表达方式及使用陷阱
- 面向对象
- 面向对象三大特征
- 类的执行顺序
- Java异常体系
- 字符串
- String与字符串常量池
- String、StringBuilder、StringBuffer的区别
- 集合
- List和Set的区别
- List的Set的排序
- hashCode()和equals()
- 输入输出流
- Java中常用的IO流
- JVM
- JVM的内存组成
- Java的垃圾回收(GC)
- 内存泄漏
基础语法
一、常用数字类型的区别
- 整型取值范围:负21到正21亿
- 整型
int
和单精度浮点型float
占用4个字节 - 长整型
long
和双精度浮点型double
占用8个字节
二、Float在JVM的表达方式及使用陷阱
运行以下代码:
float d1 = 423432423f;
float d2 = d1 + 1;
if (d1 == d2) {
System.out.println("d1等于d2");
} else {
System.out.println("d1不等于d2");
}
执行结果为:d1等于d2
- float类型,在内存中的存储形式为科学计数法,表示为:4.2343242e7,小数点后最多表示7位
- 如果需要更高精度的保存,需要使用double类型才可以
- 使用BigDecimal类进行加减乘除运算,防止因为精度问题而出现数据丢失
面向对象
三、面向对象三大特征
封装
- 将同一类事物的特征和功能包装在一起,只对外暴露需要调用的接口,最好体现封装特性的就是接口(interface)
-
在接口中只有一些方法声明,为其实现类中对接口方法的逻辑代码实现。当我们在调用时,通常是调用的接口中的方法
-
使用者只需要知道接口有哪些方法,方法有什么用就可以了,而不用要知道其实现类中具体的执行过程
封装的优势:
- 实现专业分工,使用者只要了解怎么使用就可以,开发者只要了解如何实现接口方法就可以
- 减少代码的耦合
- 对类的内部结构可以自由修改
继承(面向对象最显著的特征)
- 从已有的类中,派生新的类。新的类可以实现已有类的属性和行为,并扩展新的能力
注意:java中的类不支持多继承(一个类只能有一个父类,便于管理),而一个类可以实现多个接口 -
父类是子类的抽象总结,子类是父类的具体实现
动物 -> 食草动物 -> 羚羊、兔子、斑马 -
作为子类,既能使用父类的一些元素,又可以扩展自己的行为
实际工作中,继承主要使用在代码的抽象上,例如,现在有两个类,一个类是向表a插入数据,一个类是向b表插入数据。插入数据需要先获取数据库的链接,我们就可以针对这两个类抽象出一个父类,父类中实现数据库连接的方法。这两个子类都可以直接使用此方法 -
接口用于约束程序的行为。对于外侧使用者,暴露哪些方法
-
继承用于实现类之间代码的重用,为了程序的健壮性和可靠性服务
多态(三大特性中最重要的特性,封装和继承都是为多态服务的)
- 多态是同一个行为具有多个不同表现形式或形态的能力。多态是同一个接口,使用不同的实例而执行的不同的操作
例如:导出的接口,有导出excel和导出text文本两个实现类
四、类的执行顺序
- 静态优先
- 父类优先
- 非静态块优先于构造函数
public class ExecutionSequence {
public static void main(String[] args) {
new GeneralClass();
}
}
class ParentClass{
static {
System.out.println("①我是父类静态块");
}
{
System.out.println("②我是父类非静态块");
}
public ParentClass(){
System.out.println("③我是父类构造函数");
}
}
class GeneralClass extends ParentClass{
static{
System.out.println("④我是子类静态块");
}
{
System.out.println("⑤我是子类非静态块");
}
public GeneralClass(){
System.out.println("⑥我是子类构造函数");
}
}
执行顺序:①④②③⑤⑥
①我是父类静态块
④我是子类静态块
②我是父类非静态块
③我是父类构造函数
⑤我是子类非静态块
⑥我是子类构造函数
五、Java异常体系
- 最顶层的抽象是一个
Throwable
接口,代表了任何可以被抛出的异常或错误 Error
是操作系统级别的错误,需要终止程序运行解决- 非运行时异常需要在编码阶段进行处理(
try-catch
包裹或throw
抛出);RuntimeException
运行时异常,则不要求包裹或抛出
Error和Exception的区别与联系
字符串
六、String与字符串常量池
- 字符串一旦创建后,就会使用
final
进行修饰(不可变) -
字符串默认保存在方法区中特定开辟的区域“常量池”。当不同的String对象引用相同的字符串时,就是指向同一个内存地址
-
==
比较的是内存地址 -
equals()
方法比较的是字符串的内容
public class ConstantPool {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
String s3 = "abc"+"def";
String s4 = "abcdef";
String s5 = s2+"def";
String s6 = new String("abc");
System.out.println(s1==s2);//指向常量池中的同一个字符串,内存地址相同
System.out.println(s3==s4);//指向常量池中的同一个字符串,内存地址相同
System.out.println(s4==s5);//s2为引用类型
//Java编译器在编译阶段是无法知道引用类型的数值的,只有在运行时,s2才能确定具体的值
//s2=“abc”在加上“def”后会创建一个新的内存地址然后赋值给s5,这样s4和s5的内存地址是不相等的,所以结果为false
System.out.println(s4.equals(s5));
System.out.println(s1==s6);
//String s1 = "abc"是在程序启动时生成,存放在常量池中。
//s6 = new String("abc")是运行时创建的,所创建的字符串对象是不会在常量池中保存。所以对比内存地址的结果为false;
}
}
执行结果:
true
true
false
true
false
七、String、StringBuilder、StringBuffer的区别
- String是把数据存放在常量池中,因此在线程池中是最安全的,但是执行速度最差,推荐少量字符串操作时候进行使用
- StringBuilder执行效率最高,但是线程不安全(线程安全:在进行多线程处理时,如果多个线程对一个对象同时操作,会不会产生意料之外的结果),只推荐在单线程情况下的大量字符串操作
- StringBuffer执行速度在三者中排名中间,线程也是安全的。效率低的原因是更多考虑了多线程的情况,推荐多线程环境下操作
集合
八、List和Set的区别
- List允许重复,Set不允许重复
- List允许为null,set不允许为null
- list是有序的,set是无序的
- list的常用类用ArrayList,LinkedList;Set的常用类有HashSet,LinkedHashSet,TreeSet
1. 存储结构:ArrayList出现是作为数组的替代品,基于动态数组,在内存中所有数据都是连续的
2. LinkedList基于链表,在内存中进行松散的保存,基于指针连接在一起
3. ArrayList适用于大数据量读取(遍历),LinkedList适用于频繁更新,插入这样的写操作
1. 排序方式:HashSet不能保证顺序,TreeSet可以按预置规则排序
2. 底层存储:HashSet基于HashMap,TreeSet基于TreeMap
3. 底层实现:HashSet基于Hash表实现,TreeSet基于二叉树实现
九、List和Set的排序
List排序
- 当差值在-1至1之间时,强转后会变成0,导致结果不准确。可以加一个判断条件,当-1<差值<0时,返回-1,当0<差值<1时,返回1
public class ListSorter {
public static void main(String[] args) {
List<Employee> emps = new ArrayList<Employee>();
emps.add(new Employee("张三", 33, 1800f));
emps.add(new Employee("李四", 55, 3800f));
emps.add(new Employee("王五", 40, 2300f));
Collections.sort(emps, new Comparator<Employee>() {
// 返回正数,说明o1比o2大;返回负数,说明o2比o1大
// o2-o1是降序排列,o1-o2是升序排列
@Override
public int compare(Employee o1, Employee o2) {
return (int) (o2.getAge() - o1.getAge());
}
});
System.out.println(emps);
}
}
TreeSet排序
自然排序
- 基于JavaBean实现
comparable
接口 -
TreeSet
采用红黑树排序,compareTo()
返回-1,代表将被比较的对象放到红黑树的左边,即降序排列;返回1,代表将被比较的对象放到红黑树的右边,即升序排列
public class Employee implements Comparable<Employee> {
private String ename;
private Integer age;
private Float salary;
public Employee() {
}
public Employee(String ename, Integer age, Float salary) {
this.ename = ename;
this.age = age;
this.salary = salary;
}
@Override
public int compareTo(Employee o) {
return this.getAge().compareTo(o.getAge());//升序
//return o.getAge().compareTo(this.getAge());//降序
}
//省略getter和setter方法
}
自定义排序
- 在实例化时,实现
compartor
接口
public class SetSorter {
public static void main(String[] args) {
TreeSet<Employee> emps = new TreeSet<Employee>(new Comparator<Employee>() {
@Override
public int compare(Employee o1, Employee o2) {
return (int)(o2.getSalary() - o1.getSalary());
}
});
emps.add(new Employee("张三", 33, 1800f));
emps.add(new Employee("李四", 55, 3800f));
emps.add(new Employee("王五", 40, 2300f));
System.out.println(emps);
}
}
十、hashCode()和equals()
hashCode()
和equals()
两者都是用于比较对象是否相同,但是底层算法完全不同
hashCode()
算法很简单,就是将当前的内存地址经过哈希演算以后返回一个整数,代表“该对象的内部地址”,所以使用hashCode()
的时候生成速度非常快,但是准确性不如equals()
高,小概率情况下不同对象的hashCode()
是有可能相同的-
equals()
方法需要对对象中的各种因素进行考量,要遵循传递性,对称性,一致性等原则,所以它非常复杂,准确性也更高,但是速度比较慢
在对象间进行两者比较的原则是:
- 首先使用
hashCode()
判断两个对象是否相同,如果两个对象的hashCode()
不相等则必然不是同一个对象 -
当
hashCode()
相等的情况下,也不一定是相同的对象,需再使用equals()
方法进行验证
输入输出流
十一、Java中常用的IO流
- 字节流:二进制
- 字符流:可以阅读的字符
inputStreamReader
/outputStreamWriter
将输入/输出的字节流转化为可读的输入/输出字符流
利用IO流实现文件复制
- 实现文件复制的方式:
- 利用Java IO实现文件复制
- FileChannel实现文件复制
- Commons IO组件(Apache提供)实现文件复制
FileUtils.copyFile(Source , Target); - Java 7 提供了Files类
Files.copy(Source,Target);
public class FileCopy {
public static void main(String[] args) {
File source = new File("e:/tomcat8.zip");
File target = new File("e:/target/tomcat8.zip");
InputStream input = null;
OutputStream output = null;
try {
input = new FileInputStream(source);//文件输入字节流
output = new FileOutputStream(target);
byte[] buf = new byte[1024];//缓冲区1k字节
int byteRead;
//每次读取1k字节
while((byteRead = input.read(buf)) != -1){
output.write(buf , 0 , byteRead);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
input.close();
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
JVM
十二、JVM的内存组成
堆(Heap)
- 堆是JVM管理的内存中最大的一块,也是最主要的部分,唯一的目的是存放程序运行时所创建的对象实例
- 堆是垃圾回收的主要区域,对于不再被使用的对象(不再持有引用的对象)垃圾回收器会对其进行回收和销毁
方法区(Method Area)
- 主要存取虚拟机加载的类的信息,常量、静态变量等数据
- 方法区和堆是被所有线程共享的,开放的,可以进行直接访问
- 对于存放在方法区中的数据,垃圾回收器很少对其回收和销毁(主要回收的是常量和内型卸载的信息)对静态变量不进行回收。如果定义的静态常量是一个引用类型,那么这个被引用的对象是有可能被回收的
程序计数器
- 当前线程所执行的字节码指令的行号指示器(例如:分支、跳转、循环、异常处理、线程恢复)
- Java多线程是通过线程轮流切换分配时间片来执行的,为了线程切换后能恢复到正确的位置,所以每个线程都有单独的程序计数器。所以程序计数器是私有的(在程序进行分支、跳转时记录行号,这是计数器最典型的应用)
虚拟机栈
- 栈与堆不同,是线程私有的
- 栈主要是为Java的方法服务,当线程创建时,虚拟机栈会为线程分配一块内存的区域,在线程执行的过程中,调用的每个方法,都会创建一个栈帧(可以将“栈帧”理解为当前方法的一个引用),在栈帧中用于存放局部变量、操作栈、动态链接、方法出口等。每个方法从被调用到执行完,都对应一个栈帧,在虚拟机中从入栈到出栈的过程,我们形象的可以看成我们方法在内存中的一个实例
本地方法栈
- 与“虚拟机栈”类似
- “虚拟机栈”是为执行Java方法所提供的,而“本地方法栈”则是为执行本地方法时服务的,也就是在调用操作系统级别的底层方法时,才会在“本地方法栈”存放方法栈帧
总结
-
JVM分成共享区和私有区
-
共享区包含堆和方法区。堆用于保存程序运行时的Java变量,方法区则包含了静态内容,包括静态变量,常量,类的信息,方法的声明等,这些内容是可以被所有线程可直接访问的,所以被称为共享区
-
私有区则是指对线程来说是私有的,其他线程无法直接访问,包含程序计数器,虚拟机栈,本地方法栈
-
程序计数器是一个行号指示器,进行程序跳转的时候,我们要记录跳转的行号是多少,方便程序进行还原
-
虚拟机栈包含方法执行时的状态,每个方法都在虚拟机栈形成一个栈帧,栈帧相当于一个个方法的瞬时状态
-
本地方法栈和虚拟机栈最大区别是用途不同,本地方法栈用于调用操作系统级别的底层方法时才会在这里存放方法的栈帧;虚拟机栈则保存的是执行Java方法时对应的栈帧
十三、Java的垃圾回收(GC)
- GC(Garbage Collection)用于回收不再使用的内存
- GC负责3项任务:分配内存、确保引用、回收内存
- GC回收的依据:当对象没有任何作用,则可以被回收
- 垃圾回收器使用的是有向图的方式管理和记录堆内存中的所有对象。通过有向图可以识别哪些对象时可达的,哪些对象是不可达的,对于可达的对象进行保留,不可达的对象视为垃圾,被垃圾回收器处理掉
- 垃圾回收(GC)算法
- 引用计数算法
- JVM堆中每一个对象都有一个计数器,在引用到这个变量的时候,计数器加一,释放或断开的时候计数器减一。如果引用计数器变成0,则可以被回收。(注意:当两个对象彼此互相引用,会形成一个循环。称为循环引用,计数器是无法解决循环引用的情况的)(最简单/效率低)
- 跟踪回收方法
- 利用JVM维护的对象引用图,可以形象的理解为JVM在内存中画了一张图,从根节点开始遍历对象的引用图,同时标记还可以遍历到的对象。遍历结束后未被遍历的对象就是没被使用的对象,可以进行回收
- 压缩回收算法
- 将JVM堆中活动的对象,放到一个集中的区域中,在堆的另外一端留出大块空闲区域。相当于对堆中的碎片进行处理(性能损失大)
- 复制回收算法
- 把堆分成两个形同大小的区域,在任何时刻只有其中的一个区域被使用,直到其被消耗完。随后垃圾回收器会阻断程序的运行,通过遍历把所有活动的对象复制到另外一个区域中,复制时这些对象时紧密的挨在一起的。从而消除在内存中产生的碎片。复制完成后继续运行,直到区域被使用完,再重复上面的方法
- 优点:在垃圾回收的同时,也完成了对对象的重新布置和安排,因为对象都是紧密连接的所以其访问效率和寻址效率都非常高,并且一次性解决了内存碎片问题
- 缺点:1,需要指定2倍大小的内存空间 2,在内存调整的过程需要中断当前程序的执行,降低了程序的执行效率
- 按代回收算法(主流)
- 在程序中如果按对象生命周期的长短进行区分,绝大多数对象的生命周期都很短。比如方法中声明的对象,方法结束这个对象就被释放了,只有很少的部分对象有较长的生命周期,例如:全局变量、一些需要一直持有易用的变量才拥有较长的生命周期
- 按代回收算法的思路为把堆分成两个或者多个子堆,每个子堆都视为一代。回收算法在运行时,优先回收年轻的对象(新产生的),对于一些经过多次回收依然存活的,则把其移到高一级的堆中。这种按代分类的做法,可以减少一些稳定的、不常用的类的扫描次数,进而缩小扫描范围,提升了回收效率
十四、内存泄漏
- 内存泄漏指一个不再被程序使用的对象或者变量,还在内存中占用空间
- C、C++语言中,垃圾回收是需要手动操作的,如果程序员忘记释放,就会造成内存泄漏
- Java有垃圾回收机制,由回收器自动回收,可以极大的降低程序员垃圾回收的工作,但Java也会出现内存泄漏的场景
Java中内存泄露的场景
- 静态集合类
如果使用static
修饰了一个集合,存放的数据量又比较大,而且通常方法区的内存是比较小的,时间久了就会产生内存溢出的情况,进而程序崩溃 - 各种连接
数据库连接、网络连接、IO连接等,只打开,但未关闭。在JVM中这些对象一直是可达状态,因此不会被回收,时间久了就会导致程序崩溃 - 监听器
因为监听器往往都是全局存在的,如果在监听器中使用的对象或变量没有进行有效的控制的话,很容易产生内存泄漏 - 不合理的作用域
Java开发中有一个基本的原则是作用域最小化。变量能声明在方法中就不要声明在方法外,能用private不要用public,只在需要它的时候才去创建,如果一个变量的作用范围大于它的使用范围,很有可能导致内存泄漏的情况,另外如果在平时使用时,没有把引用对象及时设置成null,也有可能造成内存泄漏
内存泄漏和内存溢出的区别与联系
内存泄漏
指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出内存溢出
指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错,即所谓的内存溢出- 内存泄漏的堆积最终会导致内存溢出
发表回复