JavaSE
NIO和IO的区别
NIO
即New IO
,在JDK 1.4
引入,NIO
和IO
有相同的作用和目的,但实现方式不同,NIO
主要用到的是块,所以 NIO
的效率要比IO
高很多。在Java API
中提供了两套NIO
,一套是针对标准输入输出NIO
,另一套就是网络编程NIO
。
NIO
和IO
的主要区别:- 面向不同:
IO
是面向流的,NIO
是面向缓冲的。面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。Java NIO
的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。 - 阻塞和非阻塞:当一个线程调用
read()
或write()
时,该线程被阻塞,直到有一些数据被读取,或数据完全写入,该线程在此期间不能再干任何事情了。Java NIO
的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO
的空闲时间用于在其它通道上执行IO
操作,所以一个单独的线程现在可以管理多个输入和输出通道channel
。 - 选择器:
Java NIO
的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来选择通道。这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
- 面向不同:
- 总结:
NIO
可只使用一个(或几个)单线程来管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更加复杂。BIO
方式适用于连接数目比较小,且固定的架构,对服务器资源要求比较高,并发局限于应用中。NIO
方式适用于链接数目多且连接比较短(轻操作)的架构,如聊天服务器,并发局限于应用中,编程比较复杂。AIO
方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7
开始支持.
时间日期类
时间戳 ↔ 时间类
public void demo01() {
Date date = new Date();
date.getTime();// 将时间对象转化为毫秒值
}
public void demo02() {
Long time = 1547539165946;
Date date = new Date(time); // 将毫秒值转换为时间类型的对象
}
时间日期格式化
// 格式化
public void demo03() {
Date date = new Date();
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
format.format(date);// 格式化
}
// 解析
public void demo04() {
String str = "2019-01-15 16:15:33";
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = format.parse(str); //解析
}
单例模式
// 1.饿汉式(简单可用)
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance(){
return instance;
}
}
// 2.懒汉式(线程不安全,不可用)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singlenton getInstance() {
if (instance == null) {
return new Singleton();
}
}
}
// 3.同步方法的懒汉式(线程安全,但是效率低)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == nulll) return new Singleton();
}
}
// 4.双重校验锁(可用)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
return new Singleton();
}
}
}
}
}
// 5.静态内部类(推荐)
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private static final Singleton getInstace() {
return SingletonHolder.INSTANCE;
}
}
// 6.枚举类的单例模式,不常用这里暂没有列出
- 反射的方式破坏单例设计模式: 以上5种单例模式实现方式除枚举方式外,其他的通过反射的方式依然可以获取该类的实例对象,从而创建多个实例对象,可以通过在构造函数中抛出异常的方式,阻止反射的进行。
private Singleton() {
if (null != Singleton.singleton) {
throw new RuntimeException();
}
}
- 反序列化攻击破坏单例模式: 对于Java语言提供的序列化/反序列化机制,需要单例类实现
Serializable
接口;而在在反序列化时会调用实例的readResolve()
方法,只要加入该方法,并在方法中指定返回单例对象,就不会再新建一个对象,如下:
private Object readResolve() {
return Singleton.singleton;
}
E-R图
在E-R
图,也称为实体-联系图中有如下四个成分:
-
矩形框: 表示实体,在框中记入实体名。
-
菱形框: 表示联系,在框中记入联系名。
-
椭圆形框: 表示实体或联系的属性,将属性名记入框中。对于主属性名,则在其名称下画一下划线。
-
连线: 实体与属性之间;实体与联系之间;联系与属性之间用直线相连,并在直线上标注联系的类型。
(对于一对一联系,要在两个实体连线方向各写
1
; 对于一对多联系,要在一的一方写1
,多的一方写N
;对于多对多关系,则要在两个实体连线方向各写N
,M
)
排序算法
冒泡排序
public static void main(String[] args) {
int[] arr = {1, 23, 24, 32, 7, 9, 21, 10, 11, 3};
// 外循环,数组有N个,则循环N-1个
for (int i = 0; i < arr.length - 1; i++) {
// 内循环,相邻位置上的两个元素两两比较
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
System.out.println(Arrays.toString(arr));
}
选择排序
public static void main(String[] args) {
int[] arr = {1, 23, 24, 32, 7, 9, 21, 10, 11, 3};
// 外循环,循环控制次数,即数组长度 - 1
for (int i = 0; i < arr.length - 1; i++) {
// 内循环,当前元素与之后的元素依次比较
for (int j = i + 1; j < arr.length; j++) {
if (arr[i] > arr[j]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
System.out.println(Arrays.toString(arr));
}
二分查找
private static int binarySearch(int[] arr, int num) {
int min = 0;
int max = arr.length - 1;
int mid;
while (arr[mid = (min + max) / 2] != num) {
if (arr[mid] < num) {
// 当中间数小于给定值,则让最小值设置为中间数的后一位
min = mid + 1;
} else {
// 当中间数大于给定值,则让最大值设置为中间数的前一位
max = mid - 1;
}
if (min > max) {
return -1;
}
}
return mid;
}
Map的区别
-
Hashtable:
- 底层数组 + 链表,无论
key
和value
都不能为null
,线程安全,实现线程安全的方式是在修改数据时锁住整个Hashtable
,效率低,ConcurrentHashMap
做了相关优化。 - 初始
size
为11
,扩容:newsize = oldsize * 2 + 1
- 底层数组 + 链表,无论
-
HashMap:
- 底层数组 + 链表实现,可以存储
null
键和null
值,线程不安全 - 初始
size
为16
,扩容:newsize = oldsize * 2
,size
一定为2
的n
次幂 - 扩容针对整个
Map
,每次扩容时,原来数组中元素依次重新计算存放位置,并重新插入 - 插入元素后才判断该不该扩容(
Java8
及以后) - 当
Map
中元素总数超过Node
数组的75%
,触发扩容操作,为了减少链表长度,元素分配更均匀
- 底层数组 + 链表实现,可以存储
-
HashMap
的初始值还要考虑加载因子- 哈希冲突: 若干
Key
的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry
链,对Key
的查找需要遍历Entry
链上的每个元素执行equals()
比较。 - 加载因子: 为了降低哈希冲突的概率,默认当
HashMap
中的键值对达到数组大小的75%
时,即会触发扩容。因此,如果预估容量是100
,即需要设定100/0.75=134
的数组大小 - 空间换时间:如果希望加快
Key
查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。
- 哈希冲突: 若干
-
HashMap
和Hashtable
都用hash
算法来决定其元素的存储,因此HashMap
和Hashtable
的hash
表含如下属性:- 容量
(capacity)
:hash
表中桶的数量 - 初始化容量
(initial capacity):
创建hash
表时桶的数量,HashMap
允许在构造器中指定初始化容量。 - 尺寸
(size):
当前hash
表中记录的数量 - 负载因子
(load factor):
负载因子等于size/capacity
。负载因子如果为0
,表示空的hash
表,0.5
表示半满的散列表。因此,轻负载的散列表具有冲突少、适宜插入与查询的特点。 - 负载极限:是一个
0~1
的数值,决定了hash
表的最大填满程度。当hash
表中的负载因子达到指定的负载极限时,hash
表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing
。默认值为0.75
- 较高的负载极限可以降低
hash
表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作。(因为HashMap
的get()
和put()
方法都要用到查询)。 - 较低的负载极限会提高查询数据的性能,但会增加
hash
表所占用的内存开销。
- 较高的负载极限可以降低
- 容量
-
ConcurrentHashMap
- 底层采用分段的数组 + 链表实现,线程安全
- 通过把整个
Map
分成N
个Segment
,可以提供相同的线程安全,但是效率提升N
倍,默认提升16
倍。(读操作不加锁,由于HashEntry
的value
变量是volatile
的,也能保证读取到最新的值) Hashtable
的synchronized
是针对整张hash
表的,即每次锁住整张表让线程独占,ConcurrentHashMap
允许多个修改操作并发执行,其关键在于使用了锁分段技术- 有些方法需要跨段,比如
size()
和containsValue()
,它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。 - 扩容:段内扩容(段内元素超过该段对应
Entry
数组长度的75%
触发扩容,不会对整个Map
进行扩容),插入前检测需不需要扩容,有效避免无效扩容。
-
Hashtable
和HashMap
都实现了Map
接口,但是Hashtable
的实现是基于Dictionary
抽象类的。Java5
提供了ConcurrentHashMap
,它是HashTable
的替代,比HashTable
的扩展性更好。 -
HashMap
基于哈希思想,实现对数据的读写。当我们将键值对传递给put()
方法时,它调用键对象的hashCode()
方法来计算hashcode
,然后找到bucket
位置来存储值对象。当获取对象时,通过键对象的equals()
方法找到正确的键值对,然后返回值对象。HashMap
使用链表来解决碰撞问题,当发生碰撞时,对象将会储存在链表的下一个节点中。HashMap
在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode
相同时,它们会储存在同一个bucket
位置的链表中,可通过键对象的equals()
方法来找到键值对。如果链表大小超过阈值(8)
,链表就会被改造为树形结构。 -
在
HashMap
中,null
可以作为键,这样的键只有一个,但可以有一个或多个键所对应的值为null
。当get()
方法返回null
值时,即可以表示HashMap
中没有该key
,也可以表示该key
所对应的value
为null
。因此,在HashMap
中不能由get()
方法来判断HashMap
中是否存在某个key
,应该用**containsKey()
**方法来判断。而在Hashtable
中,无论是key
还是value
都不能为null
。 -
Hashtable
是线程安全的,它的方法是同步的,可以直接用在多线程环境中。而HashMap
则不是线程安全的,在多线程环境中,需要手动实现同步机制。 -
Hashtable
与HashMap
另一个区别是HashMap
的迭代器(Iterator)
是fail-fast
迭代器,而Hashtable
的enumerator
迭代器不是fail-fast
的。所以当有其它线程改变了HashMap
的结构(增加或者移除元素),将会抛出ConcurrentModificationException
,但迭代器本身的remove()
方法移除元素则不会抛出该异常。但这并不是一个一定发生的行为,要看JVM
。
-
从类图中可以看出来在存储结构中
ConcurrentHashMap
比HashMap
多出了一个类Segment
,而Segment
是一个可重入锁。ConcurrentHashMap
是使用了锁分段技术来保证线程安全的。 -
锁分段技术: 首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
-
ConcurrentHashMap
提供了与Hashtable
和SynchronizedMap
不同的锁机制。Hashtable
中采用的锁机制是一次锁住整个hash
表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap
中则是一次锁住一个桶。 -
ConcurrentHashMap
默认将hash
表分为16
个桶,诸如get
、put
、remove
等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16
个写线程执行,并发性能的提升是显而易见的。
主函数
JVM
在运行程序时,会首先查找main()
方法。- 其中,
public
是权限修饰符,表明任何类和对象都可以访问这个方法,static
表明main()
方法是个静态方法,即方法的代码是存储在静态代码区的。 - 只要类被加载后,就可以使用该方法而不需要通过实例化对象来访问,可以直接通过
类名.main()
方法直接调用。JVM
启动时就是按照上述方法声明来查找方法的入口地址,若能找到就执行,找不到就报错。 void
表明方法没有返回值,main
是JVM
识别的特殊方法名,是程序的入口方法。- 因为
main
是程序的入口方法,所以当程序运行时,第一个执行的方法就是main()
方法。 - 正因为此,此时还没有实例化一个对象,所以
main()
方法必须被定义成public
和static
。但由于public
和static
没有先后顺序,所以它俩的位置可以互换; - 也可以将
main()
方法定义为final
;也可以用synchronized
来修饰main()
方法。 - 虽然每一个类都可以定义
main()
方法,一个.java
文件可以有多个main()
方法,但是只有与文件名相同的用public
修饰的类中的main()
方法才能作为整个程序的入口方法。 - 在
Java
语言中,由于静态代码块在类被加载时就会被调用,因此可以在main()
方法执行前,利用静态代码块来实现。并且静态代码块不管顺序如何,即不管是在main()
方法前面还是main()
方法后面,都会在main()
方法执行前被实现。
Java程序初始化顺序
- 在
Java
语言中,当实例化对象时,对象所在类的所有成员变量首先要进行初始化,只有当所有类成员完成初始化后,才会调用对象所在类的构造函数创建对象。 Java
程序的初始化一般遵循3
个原则(优先级依次递减):- 静态对象(变量)优先于非静态对象(变量)初始化。其中,静态对象(变量)只初始化一次,非静态的可能会初始化多次。
- 父类优先于子类进行初始化。
- 按照成员变量的定义顺序进行初始化。即使变量定义散布在方法定义中,它们依然在任何方法(包括构造方法)被调用之前先初始化。
Java
程序初始化工作可以在许多不同的代码块中来完成,它们执行的顺序如下:父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量、父类非静态代码块、父类构造函数、子类非静态变量、子类非静态代码块、子类构造函数。
Java文件
- 一个
Java
文件可以定义多个类,但最多只能有一个类被public
修饰,且这个类的类名必须与文件名相同。 - 若这个文件没有
public
修饰的类,则文件名随便是一个类的名字即可。但是,当用javac
指令编译这个.java
文件时,它会给每一个类生成对应的.class
文件。
构造函数
- 构造函数必须与类的名字相同,并且不能有返回值,也不能有
void
。 - 每个类可以有多个构造函数。当开发人员没有提供构造函数时,编译器会在把源代码编译成字节码过程中提供一个没有参数默认的构造函数,但该构造函数不会执行任何代码。如果开发人员提供了构造函数,那么编译器就不会再创建默认的构造函数。
- 构造函数可以重载,所以它可以有
0
个(有一个默认的)、1
个或1
个以上的参数。 - 构造函数总是与
new
操作一起调用,且不能由程序的编写者直接调用,必须要由系统调用。**构造函数会在对象实例化时自动被调用,且只运行一次;**而普通方法是在程序执行到它时被调用,且可以被该对象调用多次。 - 构造函数的主要作用是完成对象的初始化作用。
- 构造函数不能被继承,因此,它不能被覆盖,更不能被重写。但是构造函数能被重载。
- 子类可以通过
super
关键字来显示地调用父类的构造函数,**当父类没有提供无参构造函数时,子类的构造函数中必须显示地调用父类的。如果父类提供了无参构造函数,此时子类的构造函数可以不显示地调用父类的构造函数,在这种情况下编译器会默认调用父类的无参构造函数。**当有父类时,在实例化对象时会先执行父类的构造函数,然后执行子类的构造函数。 - 当父类和子类都没有定义构造函数时,编译器会为父类生成一个默认的无参数构造函数,给子类也生成一个默认的无参构造函数。
clone方法
Java
在处理基本数据类型时,都是采用按值传递的方式执行,除此之外其他类型都是按引用传递(引用也是一个值)的方式执行。对象除了在函数调用时引用传递,在使用=
赋值时也采用引用传递。- 使用
clone()
方法的步骤:- 实现
clone
的类首先需要继承Cloneable
接口。这是一个标识接口,如果不实现此类,在调用clone()
方法时,则会抛出CloneNotSupportedException
。由于Object
没有实现Cloneable
接口,所以Object
类的对象调用此方法,一定会抛出上述异常。 - 在类中重写
clone()
方法。 - 在
clone
方法中调用super.clone()
。无论clone
类的继承结构是什么,super.clone()
都会直接或间接调用java.lang.Object
类的clone()
方法。 - 把潜伏着的引用指向原型对象的新克隆体。
- 实现
- 当类中包含了一些对象时,就需要用到深拷贝了,实现方法是在对该对象调用
clone()
方法完成复制后,接着对对象中的非基本类型的属性也调用clone()
方法完成深拷贝。 - 实现对象克隆
- 实现
Cloneable
接口并重写Object
类中的clone()
方法; - 实现
Serializable
接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。
- 实现
反射机制
- 反射机制是
Java
语言中一个非常重要的特性,它允许程序在运行时进行自我检查,同时也允许对其内部的成员进行操作。 - 由于反射机制能够实现在运行时对类进行装载,因此能够增加程序的灵活性。
- 反射机制提供的功能主要如下
- 得到一个对象所属的类;
- 获取一个类的所有成员变量和方法;
- 可以在运行时动态地创建类的对象。
- 在运行时调用对象的方法。
面向对象四大特征
- 抽象。抽象就是将一类对象的共同特征总结出来构造类的过程,抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。抽象分为:数据抽象和过程(控制)抽象两种形式。
- 继承。一个新类可以从现有的类中派生,这个过程称为类继承。 提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称之为子类(派生类)。
- 封装。封装是指将客观事物抽象成类,每个类对自身的数据和方法实行保护。类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏(这就是一种封装)。可以说,封装就是隐藏一切可以隐藏的东西,只向外界提供最简单的编程接口。
- 多态。多态是指允许不同类的对象对同一消息作出的不同响应(一般是不同的响应方式)。多态分为编译时的多态和运行时的多态。方法 重载
(overload)
实现的是编译时的多态性(也称为前绑定),而 方法重写(override)
实现的是运行时的多态(称为后绑定)。 - 实现多态需要做两件事
- 方法重写(子类继承并重写父类已有的方法);
- 对象造型(父类引用指向子类对象,这样就可以调用同样的方法实现不同的响应)。
重载和重写
- 在使用重载时,需注意以下几点:
- 重载是通过不同的方法参数来区分的,例如参数的个数、不同的参数类型或不同的参数类型的顺序。
- 不能通过方法的访问权限、返回值类型和抛出的异常类型来进行重载。
- 在使用重写时,需注意以下几点:
- 子类的重写方法必须要和父类中被重写的方法具有相同的函数名和参数。
- 子类的重写方法必须要和父类中被重写的方法具有相同的返回值类型。
- 子类的重写方法所抛出的异常必须和父类中被重写的方法所抛出的异常一致,不能抛出更大的异常。
- 父类中重写的方法不能被定义成
private
,否则子类只是重新定义了一个方法,并没有重写。
- 重载与重写的区别主要有以下几个方面:
- 重写是子类和父类之间的关系,是垂直关系;重载是同一个类中方法之间的关系,是水平关系。
- 重写要求参数列表相同;重载要求参数列表不同。
- 重写关系中,调用方法体是根据对象的类型(对象应用存储空间类型)来决定的;而重载关系是根据调用时的实参表与形参表来选择方法体的。
- 【补充】为什么不能根据返回值类型区分重载
- 因为调用时不能指定类型信息,编译器不知道你要调用哪个函数。当然如果声明一个类型的变量去接收,编译器可以通过上下文判断出含义,这样是没问题的;但是,我们很可能调用一个方法,同时忽略返回值,我们通常把这称为“它的副作用去调用一个方法”,这样就没办法进行判断。
- 函数的返回值只是作为函数运行之后一个“状态”,他是保持方法的调用者和被调用者进行通信的关键。并不能作为某个方法的“标识”。
内部类
- 把一个类定义到另外一个类的内部,这个类里面的这个类就叫内部类。
- 内部类有很多种,主要有以下
4
种:静态内部类(static inner class)
、成员内部类(member inner class)
、局部内部类(local inner class)
和匿名内部类(anonymous inner class)
。 - 静态内部类:指被声明为
static
的内部类,它可以不依赖于外部类而实例化,而通常的内部类需要在外部类实例化后才能实例化。静态内部类不能与外部类有相同的名字,不能访问外部类非静态成员变量,只能访问外部类的静态成员变量和静态方法(包括私有类型)。 - 成员内部类: 成员内部类为非静态内部类,它可以自由地引用外部类的属性和方法,无论这些属性是否是静态的。但是,它与一个实例绑定在了一起,不可以定义静态方法和属性。也就是说,成员内部类中,不能有静态变量和静态方法。只有外部类被实例化后,这个内部类才能被实例化。
- 局部内部类: 指的是定义在一个代码块内的类,它的作用范围为其所在的代码块,是内部类中最少使用的一种类型。局部内部类像局部变量一样,不能被
public、protected、private
以及static
修饰,只能访问方法中定义为final
类型的局部变量。(如果该变量自始至终都没有变化,这个final可以省略)。 - 匿名内部类: 一种没有类名的内部类,不能使用关键字
calss、extends、implements
,没有构造函数,它必须继承(extends)
其他类或实现其他接口。使用匿名内部类时,需要牢记以下几个原则:- 匿名内部类不能有构造函数;
- 匿名内部类不能定义静态成员、方法和类。
- 匿名内部类不能使
public、protected、private、static
。 - 只能创造匿名内部类的一个实例。
- 一个匿名内部类一定是在
new
后面,这个匿名内部类必须实现一个接口或者继承一个父类。 - 因为匿名内部类是局部内部类的一种,所以局部内部类的所有限制对其有效。
获取父类的类名
Java
语言提供了获取类名的方法:getClass().getName()
,开发人员可以调用这个方法来获取类名。而在Java
语言中任何类都继承自Object
类,getClass()
方法在Object
类被定义为final
与native
,子类不能覆盖该方法。- 因此
this.getClass()
与super.getClass()
最终调用的都是Object
中的getClass()
方法。而**Object
的getClass()
类方法的释义是:返回此Object
运行时的类**,所以无法通过此方法获取父类的类名。 - 但是,可以通过
Java
的反射机制,使用getClass().getSuperclass().getName()
来获取。
变量命名规则
- Java语言规定标识符只能由字母
a~z,A~Z
、数字0~9
、下划线_
和$
组成,并且标识符的第一个字符必须是字母、下划线_
或$
。此外,标识符也不能包括空白字符。并且Java
中变量名是区分大小写的。
final、fianlly和finalize
final
用于声明属性、方法和类,分别表示属性不可变、方法不可重写和类不可被继承。final
属性:被final
修饰的变量不可变。而不可变有两重含义:一是引用不可变,二是对象不可变。而final
指的是引用的不可变性。即它只能指向初始化时指向的那个对象,而不关心指向对象内容的变化。所以,**被final
修饰的变量必须被初始化。**一般可以通过以下几种方式对其进行初始化:- 在定义时的时候初始化;
final
成员变量可以在初始化块中初始化,但不可以在静态初始化块中初始化;- 静态
final
成员变量可以在静态初始化块中初始化,但不可以在初始化块中初始化; - 在类的构造器中初始化,但静态
final
成员变量不可以在构造函数中初始化。
final
方法: 当一个方法声明为final
时,该方法不允许任何子类重写这个方法,但子类仍然可以使用这个方法。final
类:当一个类被声明为final
时,此类不能被继承,所有方法都不能被重写。但并不表示final
类的成员变量也是不可改变的,要想final
类的成员变量不可变,必须给成员变量增加final
修饰。
finally
作为异常处理的一部分,它只能用在try/catch
语句中,并且附带一个语句块,表示这段语句最终一定被执行,经常被用在需要释放资源的情况下。finalize
是Object
类的一个方法,在垃圾回收器执行时会调用被回收对象的finalize()
方法,可以重写此方法来实现对其他资源的回收,例如关闭文件等。- 需要注意的是,一旦垃圾回收器准备好释放对象占用的空间,将首先调用其
finalize()
方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
- 需要注意的是,一旦垃圾回收器准备好释放对象占用的空间,将首先调用其
switch
switch
语句用于多分支选择,在使用switch(expr)
时,expr
表达式可以是byte、short、int、char、Enum(枚举值)、String(字符串)
(浮点型数据类型和long
类型不能作为表达式)。- 从本质上说,
switch
对字符串的支持,其实是int
类型值的匹配。它的实现原理如下:- 通过对
case
后面的String
对象调用hashCode()
方法,得到一个int
类型的hash
值,然后用这个hash
值来唯一标识这个case
。那么当匹配时,首先调用这个字符串hashCode()
函数,获取一个hash
值(int类型),用这个hash
值来匹配所有的case
,如果没有匹配成功,说明不存在;如果匹配成功了,接着调用字符串的equals()
方法进行匹配。 - 由此可以看出,
String
变量不能为null
。同时,switch
的case
子句中使用的字符串也不能为null
- 通过对
- 一般必须在
case
语句结尾添加break
语句。因为一旦通过switch
语句确定了入口点,就会顺序执行后面的代码,直到遇到关键字break
。这俗称为case
的穿透性。
abstract
- 抽象方法需要子类重写,而静态方法是无法被重写的, 因此
abstract
不能同时与static
使用。 - 本地
native
方法是由本地代码(如C
代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。 synchronized
和方法的实现细节有关,抽象方法不涉及实现细节,因此也是矛盾的;final
修饰的方法不能被重写,而抽象方法需要被子类重写,所以二者也是矛盾的。
不可变类
- 不可变类
(immutable class)
是指当创建了这个类的实例后,就不允许修改它的值了。也就是说,一个对象一旦被创建出来,在其整个生命周期中,它的成员变量就不能被修改了。 - 在
Java
类库中,所有基本类型的包装类都是不可变类,例如Integer
等。此外String
也是不可变类。 - 通常来说,要创建一个不可变类需要遵循下面
4
条基本原则:- 类中所有成员变量被
private
所修饰。 - 类中没有写或者修改成员变量的方法,例如
setXxx
。只提供构造函数,一次生成,永不改变。 - 确保类中所有方法不会被子类覆盖,可以**通过把类定义为
final
或者把类中的方法定义为final
**来达到这个目的。 - 如果一个类成员不是不可变量,那么在成员初始化或者使用
get
方法获取成员变量时,需要通过colne
方法来确保类的不可变性。 - 如果有必要,可使用重写
Object
类的equals()
方法和hashCode()
方法。在equals()
方法中,根据对象的属性值来比较两个对象是否相等,并且保证equals()
方法判断为相等的两个对象的hashCode()
方法的返回值也相等,这就可以保证对象被正确地放到HashMap
或HashSet
集合中。
- 类中所有成员变量被
- 不可变类具有使用简单、线程安全、节省内存空间等优点。 但是不可变类的对象会因为值的不同而产生新的对象, 从而导致无法预料的问题。
值传递和引用传递
- 值传递:在方法调用中,实参会把它的值传递给形参,形参只是用实参的值初始化一个临时的存储单位。因此形参与实参虽然有相同的值,但是却有着不同的存储单元,因此对形参的改变不会影响实参的值。
- 引用传递:在方法调用中,传递的对象(也可以看作是对象的地址),这时形参与实参的对象同一块存储单元,因此对形参的修改就会影响实参的值。
- 在
Java
语言中,原始数据类型在传递参数时都是按值传递,而包装类型在传递参数时是按引用传递的。
Math
round
方法表示四舍五入。round
其实现原理是在原来数字的基础上先增加0.5
然后再向下取整,等同于(int)Math.floor(x+0.5f)
。它的返回值类型为int
型。ceil
方法的功能是向上取整。ceil
意为天花板。Math.ceil(a)
,就是取大于a
的最小整数值。需要注意的是,它的返回值类型并不是int
型,而是double
型。floor
方法的功能是向下取整。floor
意为地板。Math.floor(a)
,就是取小于a
的最大整数值。它的返回值与ceil
方法一样,是double
型。
String
String s = new String("asc");
这种方式则是在堆中开辟一个新的内存空间,所以每次new
总会生成新的对象,即使这些个字符串的值是相同的。String s = "asc";
在JVM
中存在一个字符串池,其中保存着很多String
对象,并且可以被共享使用。当创建一个字符串常量时,会首先在字符串常量池中查找是否已经有相同的字符串被定义,其判断的依据是String
类equals(Object obj)
方法的返回值。若已经定义,则直接获取对其的引用,此时不需要创建新的对象;若没有定义,则首先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。- 由于
String
是不可变类,一旦创建好了就不能被修改,因此String
对象可以被共享且不会导致程序的混乱 String
是引用类型,底层是用char
数组实现的。 并且这个数组被final
关键字修饰。
数组
- 数组是指具有相同类型的数据集合,它们一般具有固定的长度,并且在内存中占据连续的空间。
- 在
Java
语言中,数组不仅有其自己的属性,也有一些方法可以被调用。 - 由于对象的特点就是封装了一些属性,同时提供了一些属性和方法,从这个角度来讲,数组是对象。
- 每个数组类型都有其对应的类型,可以通过
instanceof
来判断数据的类型。
length和length()
- 在
Java
语言中,数组提供了length
属性来获取数组的长度。 - 在
Java
语言中,length()
方法是针对字符串而言的,String
提供了length()
方法来计算字符串的长度。
finally
finally
块的作用就是为了保证无论出现什么情况,finally
块里的代码一定会被执行。- 由于程序执行
return
就意味着结束对当前函数的调用并跳出这个函数体,因此任何语句要执行都只能在return
前执行(除非碰到exit
函数),因此finally
块里的代码也是在return
前执行的。 - 此外,如果
finally
块里有return
语句,那么finally
块中的return
语句将会覆盖别处的return
语句,最终返回到调用者那里的是finally
中的return
的值。 - 由于在方法内部定义的变量都存储在栈中,当这个函数结束后,其对应的栈就会被回收,此时其方法体中定义的变量将不存在了。因此
return
在返回时不是直接返回变量的值,而是复制一份(浅拷贝),再返回。 - 对于基本数据类型和
String
类型,在finally
块中改变return
的值对返回值没有任何影响,而对引用数据类型的数据会有影响,但String
类型除外。
异常
Java
提供了两种错误的异常类,分别为Error
和Exception
,且它们拥有共同的父类Throwable
。Error
表示程序在运行期间出现了非常严重的错误,并且该错误是不可恢复的,由于这属于JVM
层次的严重错误,因此这种错误会导致程序终止执行的。此外,编译器不会检查Error
是否被处理,一个正确的程序中不应该也不允许存在Error
的。Exception
表示可恢复的异常,是编译器可以捕捉到的。它包含两种类型:检查异常(checked exception)
和运行时异常(runtime exception)
。- 检查异常是在程序中最经常碰到的异常。这种异常发生在编译阶段,
Java
编译器强制程序去捕获此类型异常,即把可能会出现这些异常的代码放到try
块中,把对异常的处理的代码放到catch
块中。如果程序没有处理Checked
异常,该程序在编译时就会发生错误无法编译。 - 对于运行时异常,编译器没有强制对其进行捕获并处理。如果不对这种异常进行处理,当出现这种异常时,会由
JVM
来处理。如果不对运行时异常进行处理,后果是非常严重的,一旦发生,要么是线程中止,要么是主程序终止。
- 检查异常是在程序中最经常碰到的异常。这种异常发生在编译阶段,
- 在使用异常处理时,还需要注意以下几个问题:
Java
异常处理用到了多态的概念,如果在异常处理过程中,先捕获了基类然后再捕获子类,那么捕获子类的代码块将永远不会被执行。- 尽早抛出异常,同时对捕获的异常进行处理,或者从错误中恢复,或者让程序继续执行。
- 可以根据实际的需求自定义异常类,这些自定义的异常类只要继承自
Exception
类即可。 - 异常能处理就处理,不能处理及抛出异常。
throw和throws
throw
throw
语句用在方法体内,表示抛出异常,由方法体内的语句处理。throw
是具体向外抛出异常的动作,故它抛出的是一个异常实例,执行throw
一定是抛出了某种异常。
throws
throws
语句使用在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常处理。throws
主要是声明这个方法会抛出某种类型的异常,让它的使用者要知道需要捕获的异常的类型。throws
表示出现异常的一种可能性,并不一定会发生这种异常。
Java Socket
- 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个双向链路的一端称为一个
Socket
。 Socket
也称为套接字,可以用来实现不同虚拟机或不同计算机之间的通信。Socket
可以分为两种类型:面向连接的Socket
通信协议**(TCP,传输控制协议)
和面向无连接的Socket
通信协议(UDP,用户数据报协议)
**。- 任何一个
Socket
都是由IP
地址和端口号组成,且端口号是唯一确定的。 - 基于
TCP
的通信过程:- 首先,
Server(服务器)
端Listen(监听)
指定的某个端口号(建议使用大于1024
的端口号)是否有连接请求; - 其次,
Client
(客户)端向Server
端发出Connect
(连接)请求; - 最后,
Server
端向Client
端发回Accept
(接受)消息。 - 一个连接就建立起来了,会话随即产生。
Server
端和Client
端都可以通过Send、Writer
等方法与对方通信。
- 首先,
Socket
的生命周期可以分为3
个阶段,打开Socket
、使用Socket
收发数据和关闭Socket
。- 在
Java
语言中,可以使用ServerSocket
来作为服务器端,Socket
作为客户端来实现网络通信。
Java序列化和反序列化
- 序列化是一种将对象以一连串的字节描述的过程,用于解决在对对象进行读写操作时所发生的问题。序列化可以将对象的状态写在流里进行网络传输,或者保存到文件、数据库等系统里,并在需要时把流读取出来重新构造一个相同的对象。
- 要实现序列化的类都必须实现
Serializable
接口。使用一个输出流来构造一个ObjectOutputStream
(对象流)对象,紧接着,使用该对象的writeObject(Object obj)
方法就可以将obj
对象写出(保存其状态)。要恢复时可以使用其对应的输出流。 - 序列化有以下两个特点
- 如果一个类能被序列化,那么它的子类也能够被序列化。
- 由于
static
(静态)代表类的成员,transient
(关键字,被其声明的变量,当对象需要存储时,它的值不需要维持),代表对象的临时数据,因此被这两种类型声明的数据成员是不能够被序列化。
- 由于序列化的使用会影响系统的性能,因此如果不是必须要使用序列化,应尽可能不要使用序列化。
- 在序列化和发序列化的过程中,
serialVersionUID
起着非常重要的作用,每一个类都有一个特定的serialVersionUID
,在反序列化的过程中,通过serialVersionUID
来判定类的兼容性。 - 在被序列化的类中显示地声明
serialVersionUID
,该字段必须被定义为static final
。这是一个良好的编程习惯,主要有以下3
个优点:- 提高程序的运行效率。 未显示声明
serialVersionUID
,那在序列化时会通过计算得到该值,浪费时间 - 提高程序不同平台上的兼容性。 由于各个平台的编译器在计算
serialVersionUID
时完全有可能会采用不同的计算方式,这就会导致在一个平台上序列化的对象在另外一个平台上将无法实现反序列化的操作。通过显示声明serialVersionUID
的方法完全可以避免该问题的发生。 - 增强程序各个版本的可兼容性。
- 提高程序的运行效率。 未显示声明
- 【扩展】怎样实现序列化部分属性
- 使用关键字
transient
来控制序列化属性,被transient
修饰的属性是临时的,不能被序列化。 - 因此,可以通过把不需要被序列化的属性用
transient
来修饰。
- 使用关键字
内存泄漏
- 在
Java
语言中,判断一个内存空间是否符合垃圾回收的标准有两个:第一,给对象赋予了空值null
,以后再没有被使用过;第二,给对象赋予了新值,重新分配了内存空间。 - 一般来说,内存泄漏主要有两种情况:一是在堆中申请的空间没有被释放;二是对象已不再被使用,但还仍然在内存中保留着。垃圾回收机制的引入可以有效地解决第一种情况;而对于第二种情况,垃圾回收机制则无法保证不再使用的对象会被释放。
- 因此,
Java
中内存的泄漏主要指的是第二种情况。主要有以下几个方面的内容:- 静态集合类, 例如
HashMap
和Vector
。如果这些容器为静态的,由于它们的生命周期与程序一致,那么容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。 - 各种连接, 例如数据库连接、网络连接以及
IO
连接。当不再使用时,需要调用close
方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement
或ResultSet
不显式关闭,将会造成大量的对象无法被回收,从而引起内存泄漏 - 监听器。 在
Java
语言中,往往会使用到监听器。通常一个应用中会用到多个监听器,但在释放对象的同时往往没有相应地删除监听器,这也可能导致内存泄露。 - 变量不合理的作用域。 一般而言,如果一个变量定义的作用范围大于其使用范围,很有可能会造成内存泄露,另一方面如果没有及时地把对象设置为
null
,很有可能会导致内存泄露的发生。 - 单例模式可能会造成内存泄露。 饿汉式单例设计模式,将对象设置为静态的,其生命周期随类加载而加载,随类消亡而消亡,这样会导致该实例无法被回收。
- 静态集合类, 例如
栈和堆
- 栈内存主要用来存放基本数据类型与引用变量。栈内存的管理是通过压栈和弹栈操作来完成的,以栈帧为基本单位来管理程序的调用关系,每当有函数调用时,都会通过压栈方式创建新的栈帧,每当函数调用结束后都会通过弹栈的方式释放栈帧。
- 堆内存用来存放运行时创建的对象。一般来讲,通过
new
关键字创建出来的对象都存放在堆内存中。由于JVM
是基于堆栈的虚拟机,而每个Java
程序都运行在一个单独的JVM
实例上,每一个实例唯一对应一个堆,一个Java
程序内的多个线程也就运行在同一个JVM
实例上,因此这些线程之间会共享堆内存。鉴于此,多线程在访问堆中的数据时需要对数据进行同步。 - 堆主要用来存放对象的,栈主要是用来执行程序的。相较于堆,栈的存取速度更快,但栈的生存期和大小必须是确定的,因此缺乏一定的灵活性。而堆却可以在运行时动态地分配内存,生存期不用提前告诉编译器,但也导致了其存取速度的缓慢。
一道JavaSE题
- IO流,序列化与反序列化,HashMap,List,steam流
随机生成 Salary {name, baseSalary, bonus}的记录,如“wxxx,10,1”,每行一条记录,总共1000万记录,写入文本文件(UFT-8编码),然后读取文件,name的前两个字符相同的,其年薪累加,比如wx,100万,3个人,最后做排序和分组,输出年薪总额最高的10组:
wx, 200万,10人
lt, 180万,8人
....
name 4位a-z随机,baseSalary [0,100]随机 bonus[0-5]随。年薪总额 = baseSalary*13 + bonus
请努力将程序优化到5秒内执行完
@Data
public class Salary {
private String name;
private int baseSalary;
private int bouns;
public Salary() {}
public Salary(String name, int baseSalary, int bonus) {
this.name = name;
this.baseSalary = baseSalary;
this.bonus = bonus;
}
}
// 生成1000w条随机的数据
public class SalaryTest {
public static void main(String[] args) throws IOException {
File file = new File("D:/upload/test.txt");
FileWriter fw = new FileWriter(file, true);
Integer count = 10000000;
while (true) {
StringBuilder name = new StringBuilder(4);
String chars = "abcdefghijklmnopqrstuvwxyz";
for (int i = 0; i < 4; i++) {
name.append(chars.charAt((int) (Math.random() * 26)));
}
Salary salary = new Salary();
salary.setName(name.toString());
salary.setBaseSalary((int) (Math.random() * 100 + 1));
salary.setBonus((int) (Math.random() * 5 + 1));
count--;
fw.write(JSON.toJSONStting(salary));
fw.write("\n");
if (count == 0) {
fw.close();
return;
}
}
}
}
// 获取所要求的数据
public class Test {
public static void main(String[] args) {
long time = new Date().getTime();
BufferedReader reader = new BufferedReader(new FileReader("D:upload/test.txt"));
String line;
MapString<String, Salary> map = new HashMap<>();
while ((line = reader.readLing()) != null) {
Salary salary = JSON.parseObject(line, Salary.class);
String key = salary.getName().substring(0, 2);
Salary result = map.get(key);
if (result != null) {
result.setBaseSalary(result.getBaseSalary()
+ salary.getBaseSalary() * 13 + salary.getBouns());
result.setBonus(result.getBonus() + 1);
} else {
result = new Salary();
result.setName(key);
result.setBonus(1);
result.setBaseSalary(salary.getBaseSalary() * 13 + salary.getBonus());
map.put(key, result);
}
}
ArrayList<Salary> values = new ArrayList<>();
Collection<Salary> co = map.values();
values.addAll(co);
// Java8,使用流排序
List<Salary> list = values.stream().sorted((s1, s2)
-> s2.getBaseSalary() - s1.getBaseSalary()).collect(Collectors.toList());
System.out.println((new Date().getTime() - time));
for (int i = 0; i < 10; i++) {
System.out.println(list.get(i));
}
}
}