Java基础复习计划(三)
散碎知识点
-
Math.round() 方法进行四舍五入计算,实现是:
Math.floor(a + 0.5f)
floor : 意为地板,指向下取整,返回不大于它的最大整数
ceil : 意为天花板,指向上取整,返回不小于它的最小整数
round : 意为大约,表示“四舍五入”,而四舍五入是往大数方向入. -
关于方法区溢出:
经常动态生成大量 Class 的应用中,Spring、hibernate 对类进行增强的时候使用 CGLib 类字节码技术 ,其他运行在 JVM 的动态语言;
常见的还有大量 JSP 或动态产生 JSP 文件的应用(JSP 第一次运行时需要编译) -
public Method[] getDeclaredMethods()
返回类或接口声明的所有方法,包括 public, protected, default (package) 访问和 private 方法的 Method 对象,但不包括继承的方法。当然也包括它所实现接口的方法。
public Method[] getMethods()
返回类的所有 public 方法,包括其继承类的公用方法,当然也包括它所实现接口的方法。 -
关于类加载器的简要分类:
引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的。
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。
系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。
tomcat 为每个 App 创建一个 Loader,里面保存着此 WebApp 的 ClassLoader。需要加载 WebApp 下的类时,就取出 ClassLoader 来使用。 -
方法内不能使用 static 修饰变量
线程
线程的五大状态:
- 新生(Born)
- 就绪(Runnable)
- 运行(Running)
- 消亡(Dead)
- 阻塞(Blocking)
创建线程的方式:
- extends Thread
- implements Runnable
控制线程的几种常见方法:
- setPriority(int)
设置线程的优先级,可选范围 1- 10,默认为 5,越大优先级越高;
没什么意义,因为只是概率而已 - static sleep(long)
- join()
当前线程邀请另一个线程优先执行,比如主线程里写xx.join();
意思就是主线程让 xx 线程执行完成后再执行,否则一直处于阻塞状态。 - static yield()
让当前线程放弃持有的时间片,直接回到就绪,当然也有可能出现放弃时间片后又被 Cpu 选中的情况。 - setName() + getName()
- static activeCount()
得到程序中所有活跃线程的总数,活跃线程:就绪 + 运行 + 阻塞;
这个方法永远不可能返回 0,至少是 1. - static currentThread()
得到当前线程对象,比如获得主线程的对象,在 run 方法调用的其他方法中使用;
在 run 方法中没必要,直接 this 就是了。 - setDaemon(true)
设置成为守护进程,当程序中只剩下守护线程时会自动终结自己;
Java 中著名的守护线程 GC,一般的特性:- 通常是无限循环的
- 守护线程一般有极低的优先级
- 设置守护线程必须在 start 之前
- interrupt()
中断线程的阻塞状态,比如 sleep 时间还没到可以用 interrupt() 强制唤醒,但是会抛出一个异常。
线程中所有静态方法不关注谁调用的,而是关注出现在哪里,出现在哪里就是操作那个线程。
线程中所有涉及主动进入阻塞状态的方法都需要进行异常处理
关于锁
锁的出现就是为了解决并发错误,当多个线程共享同一个对象的时候,某一个线程未处理完成时 CPU 时间片就用尽了,然后就会出现并发错误。
然后就需要加锁来保证不会出现错误,通常有两种方案:
- 使用 synchronized 关键字
叫做互斥锁,或者互斥锁标记,它可以修饰方法或者代码块,用在代码块上要显式的声明锁,用在方法上默认是 this。
还有就是 synchronized 特性本身不会被继承 - java.util.concurrent.locks.ReentrantLock
可翻译为可重用锁,JDK1.5 加入的,遵循了 OO 思想,有两个方法:lock() 和 unlock()
锁如果使用不当就会形成死锁,要解决死锁一般需要用到 Object 的三个方法:
- wait()
让当前线程放弃已持有的锁标记,并且进入调用方对象的等待池。 - notify()
唤醒调用方对象中等待池中的某个线程,是随机的。 - notifyAll()
唤醒调用方对象中等待池中的全部线程
这三个方法都必须在已经持有锁标记的前提下才能使用,所以它们都必须出现在synchronized(){当中}
锁池和等待池
利用每一个对象都有一个锁旗标,拥有这个旗标后才可以访问此对象的资源,当线程无法获取此对象的锁旗标时就会发生阻塞进入此对象的锁池,等待旗标的释放,当释放后所有的等待旗标的线程会被唤醒,进入就绪状态争夺旗标。
使用 wait 会进入等待池,遇到 synchrnized 会进入锁池;
进入等待池会释放当时持有的锁,而锁池不会;
锁池中,只要锁标记再度可用 线程自动离开,等待池 必须要 notify() 或者 notifyAll();
关于离开的去向:离开锁池前往就绪;离开等待池前往锁池(之前释放了锁,必须得重新获取锁,既然有人唤醒它,说明此时旗标肯定在别人手里)
关于线程池
关于这一块,之前在 这里 已经写过了,这次提提怎么使用就够了:
常规使用,用 ExecutorService 这个接口来写吧,以及线程的几种定义方式:
public class TestThreadPool{
public static void main(String[] args) throws Exception{
ExecutorService es = Executors.newFixedThreadPool(2);
// newCachedThreadPool();
// newSingleThreadExecutor()
ThreadOne t1 = new ThreadOne();
es.submit(t1);
ThreadTwo t2 = new ThreadTwo();
es.submit(t2);
ThreadThree t3 = new ThreadThree();
Future<String> f = es.submit(t3);
System.out.println(f.get());
//es.shutdown();
es.shutdownNow();
}
}
class ThreadThree implements Callable<String>{//JDK5.0
@Override
public String call() throws Exception{
for(int i = 0;i<6666;i++){
System.out.println("我是第三种方式");
}
return "End";
}
}
class ThreadTwo implements Runnable{
@Override
public void run(){
for(int i = 0;i<6666;i++){
System.out.println("我是第二种方式");
}
}
}
class ThreadOne extends Thread{
@Override
public void run(){
for(int i = 0;i<6666;i++){
System.out.println("我是第一种方式");
}
}
}
线程池的创建官方推荐使用 Executors 来创建,常见的有 newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor;
第一种就是最简单的,也是最常用的,会事先维护几个线程,等任务来直接执行;
第二种是当有任务的时候再创建线程(避免浪费),任务执行完后会等待一分钟(默认),如果一分钟内没有新任务,那么此线程就会被终结;
第三种是同一时间只允许一个线程执行,其他的任务都排队等着,适合用在秒杀的情况。
shutdown 和 shutdownNow 的区别:
shutdown :不再接受新的任务,当线程池中的任务(包括等待中的和正在执行的)执行完毕后销毁线程池。
shutdownNow:试图停止所有正在执行的活动任务(一般情况正在执行的任务都会正常跑完的),暂停处理正在等待的任务,不再接受新任务,并返回等待执行的任务列表。
还有就是通过实现 Callable<>
的 call 方法来定义线程,这种方式定义的线程解决了其他方式无法实现的问题:
- run() 被定义为 void 无法返回数据
- run() 没有任何 throws 声明
这种方式定义的线程,只能通过线程池的方式来启动,并且 submit 的时候会返回一个 Future 对象,利用这个对象可以获得线程的返回值,就是调用其 get 方法,注意:当线程未执行完时,此方法一直是阻塞状态;当线程被意外终止那么 get 方法可能会一直卡在阻塞中(当然有重载可以指定等待的最大时间)。
IO
这就是通常我们所说的 IO 流了,流可分为三类:
- 方向分: 输入流 or 输出流
- 单位分: 字节流 or 字符流
- 功能分: 节点流 or 过滤流(包装流、处理流)
File对象
创建 File 对象的三种常见形式:
new File(String 完整路径);
new File(String 父目录,String 文件名);
new File(File 父目录对象,String 文件名);
然后介绍常用的几个方法:
-
static listRoots()
: 得到当前计算机所有根目录 -
String[] list()
: 动态的列出一个目录当中所有的文件名字 -
File[] listFiles()
: 动态的列出一个目录当中所有的文件对象 -
exists()
: 判断 File 对象指代的文件或者目录是否存在 -
isFile()
: 判断 File 对象指代的是不是一个文件 -
isDirectory()
: 判断 File 对象指代的是不是一个目录 -
length()
: 得到文件的字节个数;只能对文件调用,对目录调用得到的没有意义 -
mkdirs()
: 创建多层不存在的目录结构 -
getName()
: 得到文件或者目录的名字 -
getParent()
: 得到文件或者目录的父目录 -
getAbsolutePath()
: 得到文件或者目录的绝对路径 -
setLastModified()
: 设置文件的最后一次修改时间,设置的是时间戳 -
lastModified()
: 得到文件的最后一次修改时间 -
delete()
: 删除目录或者文件
如果要删除的是一个目录 则必须保证目录是空的 -
renameTo()
: 重命名文件或者目录
例如:a.renameTo(c);
a 代表源文件,必须存在;c 代表目标文件,必须不存在;
其中 a 和 c 可以是不同的目录结构,从而实现剪切。
过滤器
使用 File 的 listFiles() 方法的时候可以传入一个文件过滤器(FileFilter),用来过滤指定的文件,这样能减轻接下来遍历的压力。
FileFilter 是个接口,并且它只定义了一个方法:boolean accept(File pathname)
比如:
class JavaFilter implements FileFilter{
private JavaFilter(){}
private static JavaFilter jf = new JavaFilter();
public static JavaFilter getFilter(){
return jf;
}
@Override
public boolean accept(File f){
return f.isFile() && f.getName().toLowerCase().endsWith(".java");
}
}
class DirFilter implements FileFilter{
private DirFilter(){}
private static DirFilter df = new DirFilter();
public static DirFilter getFilter(){
return df;
}
@Override
public boolean accept(File f){
return f.isDirectory();
}
}
// 使用 lambda
File[] files = file.listFiles(f -> f.isFile() && f.getName().endsWith(".java"));
File[] dirs = file.listFiles(f -> f.isDirectory());
if(files == null) return;
一个来过滤 Java 文件,一个来过滤目录,这里使用单例模式就比较适合了,另外,还可以直接使用 lambda 表达式,更加的爽
字节流
首先要认识的两个对象是:InputStream 和 OutputStream,他们分别是:所有字节输入流统一的父(抽象)类、所有字节输出流统一的父(抽象)类。
方法一览:
// 一次读一个字节,并返回这个字节
int read();
// 一次读一个数组,返回读取的长度
int read(byte[] data);
// 一次读一个数组,从 off 开始填充,填充 len 个
int read(byte[] data,int off,int len);
write(int data);
write(byte[] data);
write(byte[] data,int off,int len);
需要注意:一次读一个字节但是返回的是 int,这是为了确保返回值 -1
表示文件的结束,假设读到了一个字节是 -1,那么先会进行类型提升到 int,这里的提升是 &0xff
来确保前面补的是 0 而不是 1。
读取数组时,如果读到最后不足一个数组的大小,那么返回的 int 就不是 data.length,所以说并不是绝对的。
上面说的是顶级的抽象类,下面就来说说最常用的两个具体类:FileInputStream 和 FileOutputStream。
-
它们作为节点流,构造方法可以指定连接 String 文件名 File 对象
-
虽然贵为节点流,但是它们只能连接文件 不能连接目录
-
节点输出流连接的文件即便不存在在,创建流的时候也会被自动创建出来,但是如果连接的目录结构都不存在 则直接异常【File 类还有个 mkdirs()】
-
节点输出流连接的文件即便已经存在,在创建流的一刻 也会被新的空白文件直接替换。
如果我们的需求是想要在最后追加内容,那么构造方法:new FileOutputStream("abc.txt",true);
-
FileInputStream 最常用的是
read(byte[] data);
;FileOutputStream 最常用的却是write(byte[],int,int)
-
FileInputStream 以 -1 作为读取结束的标识
下面看经典的复制文件的例子:
public class FileCopy{
public static void main(String[] args){
FileInputStream fis = null;
FileOutputStream fos = null;
try{
fis = new FileInputStream("1.mp3");
fos = new FileOutputStream("2.mp3");
// 2^10 = 1024 也就是字节 <<10 是 kb,再 <<10 是 mb
byte[] data = new byte[5<<20]; // 5MB
int len = 0;
while((len = fis.read(data)) != -1){
fos.write(data,0,len);
}
}catch(Exception e){
e.printStackTrace();
}finally{
try{
fis.close();
}catch(Exception e){
e.printStackTrace();
}finally{
try{
fos.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}
}
// 简写
public class FileCopy2{
public static void main(String[] args){
// JDK7+ 特性。
try(FileInputStream fis = new FileInputStream("1.mp3");
FileOutputStream fos = new FileOutputStream("3.mp3")){
byte[] data = new byte[5<<20];
int len;
while((len = fis.read(data)) != -1){
fos.write(data,0,len);
}
}catch(Exception e){e.printStackTrace();}
}
}
设置缓冲大小使用位运算效率更高哦,只需要记住 x<<20 就是 xMB 大小的缓冲区。
JDK7 后有了特性,就不用在 finally 里写这么恶心的代码了。。。。
过滤流
之所以称它们为过滤流是因为他们接收的对象是 stream,并不能直接传 file 或者路径,对于字节流来说,有几个还算用的多的字节流过滤流:
BufferedInputStream、BufferedOutputStream、DataInputStream、DataOutputStream、ObjectInputStream、ObjectOutputStream。
经过过滤(包装)后,后面的操作只需要对这个过滤流操作就行了,因为是装饰(包装)模式,最后关流的时候只关过滤流就 OK 了。
前两个:
作为过滤流的它们是为了给原本的节点流添加缓冲空间,从而提高每次读写的吞吐量 进而提高效率。它们构造方法的第二个参数可以指定缓冲空间大小(默认只有8192字节,也就是 8k);一定记得及时清空缓冲空间 防止数据滞留缓冲区,其中有三种情况会刷新缓冲区:1、当缓存空间满了的时候自动刷新;2、当关闭流操作时会自动刷新;3、手动调用 flush。
这样,即使你调用 read 方法一个字节一个字节的读其实它会一次读指定的大小到缓冲区,然后一个个的给你;写也是,并不是一个个的写,而是写到缓冲区,满了以后一次性 flush 到硬盘。
中间两个:
是为了给原本的节点流添加读写基本数据类型的功能;DataInputStream 提供了一组方法 readXxxx();
,DataOutputStream 提供一组方法 writeXxxx();
,这时候不再以 -1 作为读取结束的标识了,而是如果已经到达文件结尾还继续读取,则直接出现 EOFException(End of File)。
public static void main(String[] args){
try(DataOutputStream dos = new DataOutputStream(new FileOutputStream("t.data"))){
dos.writeInt(4545);
}catch(Exception e){e.printStackTrace();}
try(DataInputStream dis = new DataInputStream(new FileInputStream("t.data"))){
int x = dis.readInt();
System.out.println(x);
}catch(Exception e){e.printStackTrace();}
}
最后两个:
给原本的节点流添加读写对象的功能的,与上面类似,对应的方法就是 readObject();
和 writeObject();
。
同样不以 -1 作为结束,而也会 EOFException。
要写出的对象必须先要序列化 (implements Serializable),如果要持久化的对象当中有其它引用类型的属性,那么也要进行序列化标识;但是如果某些属性无关紧要,不需要保存(那就相当于是 null 了),可以直接使用 transient 修饰。
如果要持久化的是一个使用了比较器的 TreeSet 或者 TreeMap,就连比较器的类型也要实现序列化接口。
字符流
和字节流一样,两大鼻祖:Read 和 Writer;接口中定义的方法也和字节流中的那三个对应,就不写了。
两个常用的具体类:FileReader 和 FileWriter,描述也不多说了,和上面 FileInputStream 和 FileOutputStream 一样一样的
过滤流
对于字符流来说,最常用的还是莫过于这里说的过滤流,因为对于字符操作,过滤流提供了更方便的方法。
BufferedReader 和 BufferedWriter:
通过对原有的字符流添加缓冲空间,使其可以支持一次读取一行(readLine),和一次写入一个字符串(write + newLine 换行)。其中 BufferReader 使用的非常频繁,必须要熟练的。
PrintStream 和 PrintWriter:
像是一对姐妹,他们的方法一致,不同的是一个输出字节,一个输出字符;PrintStream 我们最长见的就是打印语句中的 out,它就是 PrintStream 类型的,然后 PrintWriter 在 IO 操作中用的非常频繁,相比 BufferedWriter 它更加的好用。
既然说好用,那就来看看它的特点吧:
-
既可以作为节点流,又可以作为过滤流;也就是可以直接往构造函数里扔文件对象、路径、节电流
-
既可以连接字节流,又可以连接字符流;是的,构造函数里都可以扔,不需要转换流
-
当做节点流的时候,构造方法第二个参数可以指定字符集
-
当做过滤流的时候,构造方法第二个参数可以指定自动清空缓冲
例如:new PrintWriter(new FileWriter("a.txt",true),true);
第一个 true 是开启追加模式,第二个是自动刷新(flush) -
拥有 println() 方法,等价于 write() + newLine()
转换流(桥转换器):InputStreamReader 和 OutputStreamReader ,其中最常用的是 InputStreamReader,实际用法例如:
new BufferedReader(new InputStreamReader(new FileInputStream("a.txt")))
而 OutputStreamReader 基本不怎么用,因为有 PrintWriter 啊,它可以字符流字节流通吃,也就是说内部会内置一个转换流,就是这个 OutputStreamReader ,所以让我们方便了。
再来补充个 RandomAccessFile 用来支持随机文件的读取和写入,通常可以用它来占空间,然后用流来对其进行写:
RandomAccessFile raf = new RandomAccessFile("d:\\abc.mp4","rw");
File d = new File("d:\\");
long free = d.getFreeSpace();//得到d盘的剩余空间
raf.setLength(free);
raf.close();
这只是个简单的使用,这里先 TODO
原码反码补码
对于一个数, 计算机要使用一定的编码方式进行存储. 原码, 反码, 补码是机器存储一个具体数字的编码方式.
原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值,原码是人脑最容易理解和计算的表示方式。
[+1]原 = 0000 0001
[-1]原 = 1000 0001
正数的反码是其本身;负数的反码是在其原码的基础上, 符号位不变,其余各个位取反.
[+1] = [00000001]原 = [00000001]反
[-1] = [10000001]原 = [11111110]反
正数的补码就是其本身;负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)
[+1] = [00000001]原 = [00000001]反 = [00000001]补
[-1] = [10000001]原 = [11111110]反 = [11111111]补
那么为何要使用原码, 反码和补码?
首先, 因为人脑可以知道第一位是符号位, 在计算的时候我们会根据符号位, 选择对真值区域(可理解为不加符号位的二进制表示)的加减。
但是对于计算机,加减乘数已经是最基础的运算,要设计的尽量简单; 计算机辨别 "符号位" 显然会让计算机的基础电路设计变得十分复杂!
于是人们想出了将符号位也参与运算的方法; 我们知道, 根据运算法则减去一个正数等于加上一个负数, 即: 1-1 = 1 + (-1) = 0 , 所以机器可以只有加法而没有减法,这样计算机运算的设计就更简单了.
于是人们开始探索 将符号位参与运算, 并且只保留加法的方法, 首先来看原码:
计算十进制的表达式: 1-1=0
1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2
如果用原码表示, 让符号位也参与计算, 显然对于减法来说, 结果是不正确的;这也就是为何计算机内部不使用原码表示一个数.
为了解决原码做减法的问题, 出现了反码:
计算十进制的表达式: 1-1=0
1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0
发现用反码计算减法, 结果的真值部分是正确的.
而唯一的问题其实就出现在 "0" 这个特殊的数值上, 虽然人们理解上 +0 和 -0 是一样的,但是 0 带符号是没有任何意义的, 而且会有 [0000 0000]原 和 [1000 0000]原 两个编码表示 0.
于是补码的出现, 解决了 0 的符号以及两个编码的问题:
1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [0000 0000]补=[0000 0000]原
这样 0 用 [0000 0000] 表示, 而以前出现问题的 -0 则不存在了;而且可以用 [1000 0000] 表示 -128:
(-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]补 + [1000 0001]补 = [1000 0000]补
-1-127 的结果应该是 -128, 在用补码运算的结果中 [1000 0000]补 就是 -128, 但是注意因为实际上是使用以前的 -0 的补码来表示 -128, 所以 -128 并没有原码和反码表示(所以可以多表示一个最低数).(对 -128 的补码表示 [1000 0000]补 算出来的原码是 [0000 0000]原, 这是不正确的)
使用补码, 不仅仅修复了 0 的符号以及存在两个编码的问题, 而且还能够多表示一个最低数; 这就是为什么 8 位二进制, 使用原码或反码表示的范围为 [-127, +127], 而使用补码表示的范围为 [-128, 127].
因为机器使用补码, 所以对于编程中常用到的 32 位 int 类型,可以表示范围是: [-231, 231-1] 因为第一位表示的是符号位.而使用补码表示时又可以多保存一个最小值.
出自:https://www.cnblogs.com/zhangziqiu/archive/2011/03/30/ComputerCode.html
其他
查找一个字符串出现多少次?
可以使用 (str + "l").split("abc").length - 1
,源字符串加任意一个字符防止被 split 的字符在最后会导致少一个,然后它的 length - 1 就是个数。