0%

前言

每次我忘带充电器,而不得不拿着非原装充电器使用的时候,心里总是想:这么充不会把我手机给充坏了吧;每次问他人借数据线的时候,总是不知道该说借哪种线,今天打算花点时间好好整理一波。

阅读全文 »

受到疫情影响,出不了门,肯德基还一度关门….没办法,只能自己在网上购买冰冻的食物来自己做了。

我稍微调查了一下肯德基,麦当劳,汉堡王这三家的供应商,然后去网上搜了一下购买。

阅读全文 »

lambda

在正式开始Java的多线程之前,首先了解一下这个在jdk8中正式加入的为了简化操作而加入的lambda表达式。

为了便于理解,打算从最简单的开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ThreadTest {
public static void main(String[] args) {
LambdaTest lambdaTest = new Lambda();
lambdaTest.test();
}
}

interface LambdaTest {
void test();
}

class Lambda implements LambdaTest {
@Override
public void test() {
System.out.println("in class lambda");
}
}

上面这段代码是比较简单的,自己定义了一个接口,且这个接口只有一个方法(这对lambda是必须的,更加精确的说法应该是仅包含一个抽象方法),然后有一个类去实现了这个接口。最后我们在主类中调用了一下接口。

阅读全文 »

前言

之前学习Java的时候并没有学到io这一块,然后之后用到的时候都是随手网上找一段封装好的代码直接使用,有心去了解过一下,但是看到java.io包下那庞大的类,想想暂时先放下了;这次趁着春节疫情假期延长的时间来好好复习一下,最后发现还是蛮简单的。

各种语言的IO

这里我选取了C、Python和Java这三种语言来作为例子,看看它们三者之间的io之间的区别。

C语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main() {
FILE *fp = NULL;
char buff[255];

fp = fopen("/tmp/read.txt", "r");
fscanf(fp, "%s", buff);
fgets(buff, 255, fp);
printf("%s", buff);
fclose(fp);

fp = fopen("/tmp/write.txt", "w+");
fprintf(fp, "This is testing for fprintf...\n");
fputs("This is testing for fputs...\n", fp);
fclose(fp);
}

C语言的操作很简单,首先由一个指针指向一个文件,然后使用特定的函数对其进行读写,最后对文件进行关闭即可。

python

1
2
3
4
5
6
7
8
9
#!/usr/bin/env python
# -*- coding:utf-8 -*-

if __name__ == '__main__':
with open("/tmp/read.txt", "r") as f:
print(f.read())

with open("/tmp/write.txt", "w") as f:
print(f.write("test info"))

显然Python更简单…..指定好文件,然后进行操作就行了。

Java

读取文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
InputStream is = null;
File file = new File("/tmp/read.txt");
try {
is = new FileInputStream(file);
byte[] buf = new byte[1024];
int len;
while (-1 != (len = is.read(buf))) {
System.out.print(new String(buf, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null!= is){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

写出文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
OutputStream os = null;
File file = new File("/tmp/write.txt");
try {
os = new FileOutputStream(file);
os.write("测试信息".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if(null!=os){
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

看上去比上面的两位都复杂一点,但是不要忘了,主要是因为Java加了不少的异常处理机制,使得代码更加健壮。

一点小总结

从写代码的效率来说,Python必然是最优选择,而C语言和Java在写代码方面我个人觉得差距不大(因为C这里没有异常处理机制)。但是对于初学者来说,可能还是C和Python这两种语言更符合大家的常理:打开文件,对文件进行处理,最后关闭文件。Java看上去似乎很奇怪,并不是直接对文件进行操作,而是用了Stream来进行处理。这里有个问题,为什么Java要这么做?emmmmm其实我到现在还是无法理解,我个人觉得一种可能的原因是,Java使用了虚拟机,导致它无法直接对文件进行操作,所以用到了流来间接进行处理。

###字符和字节

这里还是有必要来介绍一下这两者的区别。

一个字节(byte)是八个比特,所以其本质上,是一个数字,一个从0到255的数字。在Java中有一个基本的数据类型就是byte,但是由于Java不支持无符号数据类型,所以在Java中,byte这个数据类型所能表达的数字范围是-128~127(这里多嘴提一句,byte数据一般也用来表示ip地址,所以看到负数不要惊讶)下面的一张图很能说明问题:

image-20200217182247007

而字符,在Java中是用char来进行表示的,一共是2个字节。显然能够表示65536个字符。然后在《Thinking in java》这本书里,居然说char能够表示所有的中文字符。这句话显然是错误的,光是汉字就不止六万多字,更别说还需要包括别的语言的字符了。当然如果只是常用字符,确实char是完全能够表示的。

###编码和解码

定义

维基百科对其的定义是:

编码信息从一种形式格式转换为另一种形式的过程;解码则是编码的逆过程。

也就是说,其实两者是等价的。但是习惯上,我们把从字符到字节成为编码(encode),从字节到字符称为解码(decode)。

编码和解码其实特别简单,只要我们遵守编码和解码采用相同的字符集,就不会有问题。假设现在有这么一套编码,它会把编码成 0100 0001(十进制的65),把编码成0100 0010(十进制的66)到电脑中保存。然后如果程序读取的时候,它按照了ASCII进行解码,就会解读出AB,也就是”乱码”了——即你明明写了你好在文件里,但是程序读出来却成了另外的内容(AB)。

字符集

所以关键其实在于这个字符集的选取,字符集那就首先从ASCII讲起。上世纪六十年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。而这套编码正好规定了128个字符,也就是说只用7个比特即可表示(0000 0000 - 0111 1111),而之前也提到Java中最高位代表的是符号位,所以ASCII用byte的正数表示刚刚好。

由于这套编码最多也只能由256个字符,对于中文这样的文字来说肯定是不够的,所以经过一系列的发展,有了Unicode这个字符集,囊括了世界上所有的文字(甚至连emoji都包括其中),这样相当于计算机王国有了自己的普通话标准,只要大家都使用这本大字典,就不会有问题。

看似问题是不是解决了呢?现在有了完全不会冲突的字符集,那我只需要按照字符集里每个字符所对应的二进制代码去操作,那不是完全不会有乱码了吗?没错,理论上完全没有问题。但是实际中并不能这么操作。因为为了能够表达所有的字符,其实Unicode对大部分字符的编码长度是2~4个字节(为了方便说明,我们姑且认为全是3个字节吧),而如果我仅仅为了传输一份纯英文的文档,或者说掺杂了少数中文的文档,那么开销会非常大。于是,我们就有了utf-8。这里需要注意的是:UTF-8 是 Unicode 的实现方式之一。而utf-8也是采用了所谓的变长编码方式以充分利用空间,具体实现过程这里不做赘述。所以如果无视硬盘空间和网络流量,那么其实unicode编码是最舒服最容易理解的(实际上可并没有这种编码方式哦)。

各种语言的编码和解码demo

C

根据你所定义的类型来决定。引自知乎:

char c[] = u8”I’m a UTF-8 string.”; // utf8编码 (假如是C++,那么C++11起为char,C++20起改为char8_t类型)

char16_t d[] = u”This is a UTF-16 string.” // UTF-16编码

char32_t e[] = U”This is a UTF-32 string.” // UTF-32编码

python
1
2
byte = "字符".encode("utf-8")
print(byte.decode("utf-8"))
java
1
2
3
String s = "中文";
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
System.out.println(new String(bytes, StandardCharsets.UTF_8));

总结

想要不乱码?很简单,编码和解码采用同一种编码方式即可。而往往实际中困扰我们的是,xxx编码格式兼容xxx编码格式,xxx编码格式部分兼容xxx编码格式。还有就是回到java的char这里,char是一个字符,我们如果使用数字来对char进行赋值,在Java内部是会发生解码的(从字节到字符),而我查阅了一下数据,java默认用的就是utf-8。可以通过下面的代码查看:

1
System.out.println("Default Charset=" + Charset.defaultCharset());

流处理

前面用了不少篇幅来讲述字符和字节、编码和解码相关的信息,这是因为这些其实在IO中还是很重要的(而且我之前也不是非常了解2333333),从这里正式介绍Java中的各种流。

InputStream和OutputStream

首先用Stream结尾的一定是字节流,而从它们俩的名字来也能一眼看出它们的作用。这里需要注意的是,这两个类几乎是所有处理流的父类,所以这里用多态非常容易。

InputStream可以读取单个字节,读到最后一个字节的时候会返回-1,由此来判断。当然更多的时候,是我们自己创建一个字节数组,然后读取字节到字节数组中,会返回实际读取到的字节数,来进行处理。

OutputStream需要注意的是,记得使用flush进行数据的强制写入硬盘(文件),否则可能会看不到结果。

FileInputStream和FileOutputStream

上面的两个的实现类而已,如果我们需要读取/写入文件,记得来操作这两个类。

Reader和Writer

专门用来操作字符的抽象类,会比字节流方便一丢丢。

FileReader和FileWriter

具体的实现类。

Buffered相关

装饰类,所以只需要把需要装饰的丢入即可。这里注意的是装饰之后新增了一个readLine()方法。

DataInputStream和DataOutputStream

主要是为了针对基本数据类型。

ObjectInputStream和ObjectOutputStream

序列化和反序列化操作对象。记得实现一个空接口。

Commons IO

实际一般使用这个第三方库。这里介绍下IDEA如何使用这个库:

  1. 自己下载好jar包。下载地址:http://commons.apache.org/proper/commons-io/ 解压一下。然后回到IDEA,使用File->Project structure,然后选择libraries,导入即可。
  2. 当然你可以让IDEA在maven上帮你下载,只需要在选择libraries的时候选择从maven下载即可(虽然这速度慢的可以)

问题产生

在Mac上使用了jetbrain家的toolbox更新了一下IDEA之后,发现我GitHub保存的密码没了。然后我就老老实实去生成了一个token,然后进入Setting - Version Control - Github下面,填了进去。当然没有问题,显示了我GitHub的账号。我点击了保存并且退出了设置。

当我写完代码开始往GitHub上面上传的时候,又提示我需要输入账号和密码。很奇怪,我又打开了GitHub的设置界面,发生上面赫然写着Missing access token,我一开始以为自己没保存,又重新输入了一遍,这次确认保存并退出了,结果还是要我重新输入一遍密码…..

阅读全文 »

前言

日常生活中有的时候会需要用到脚本来进行一些便利的操作,比如抢购某些东西之类的。而商家为了防范用脚本来进行操作,一般会使用一些防范措施。其中最常见的应该就是验证码了。

全自动区分计算机和人类的公开图灵测试(英语:Completely Automated Public Turing test to tell Computers and Humans Apart,简称CAPTCHA),俗称验证码,是一种区分用户是计算机的公共全自动程序。

现在的验证码越来越多,比如滑动滑块让你拼图的、直接给你手机发短信的等,这篇博客并不打算涉及这些比较复杂的,只是简单介绍一下简单的验证码以及如何去使用代码来实现简单的验证码识别。

阅读全文 »

本博文对应《深入理解java虚拟机》第七章的内容。

概述

java编译器把java文件变成了字节码,而JVM则是首先把字节码加载到内存,对数据进行校验、转换解析和初始化,最终就可以形成被虚拟机直接使用的java类型。不像C在编译的时候进行了连接工作(这里主要说的是静态链接),在java中加载、连接和初始化其实都在程序运行的时候完成的,虽然会耗时间,但是能够动态加载,这样大大提高了灵活性。

类在内存中的生命周期

类从开始被加载到内存到被从内存中移除为止,一共要经历七个步骤:

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

其中验证——准备——解析这三个步骤被称为连接(linking);而如果是为了能够支持java的运行时绑定,还可以把解析放到初始化后面。

有且仅有以下的六种情况必须进行初始化:

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令的时候,要是对应的类没有初始化,则需要先让它初始化。很明显,你创建一个类的对象总不能没有这个类吧?你设置一个类的静态属性的时候,总不能没有这个类吧?你调用一个类的静态方法的时候,总不能没有这个类吧?
  2. 对类进行反射调用的时候,肯定也需要对类进行初始化。
  3. 当要初始化一个类的时候,首先需要初始化它的父类。
  4. 主类需要先被初始化。
  5. MethodHandle实例最后的解析结果是REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有初始化,则需要首先触发初始化。
  6. JDK8新增了接口的默认实现,如果这个接口的实现类发生了初始化,那么接口要先初始化。

上面这五种都被称为主动引用,接下来用三个例子说明什么是被动引用。

例子1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SuperClass {
static {
System.out.println("Super Class init");
}

public static int value = 123;
}

public class SubClass extends SuperClass {
static {
System.out.println("Sub Class init");
}
}

public class Test {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}

只会输出Super Class init123,也就是说虽然获取的是subclass类的值,但是并没有初始化subclass。因为对于静态字段,只有直接定义这个字段的类才会被初始化。

例子2:

1
2
3
4
5
public class Test {
public static void main(String[] args) {
SuperClass[] superClasses = new SuperClass[3];
}
}

你会发现什么输出都没有。

例子3:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ConstClass {
static {
System.out.println("Const Class init");
}

public static final String HELLOWORLD = "hello world!";
}

public class Test {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}

你会发现只输出了hello world!但是没有输出Const Class init那段代码,这是因为在编译阶段及逆行了常量优化,直接把那个字符串常量存储到了Test类中的常量池里面。也就是这两个类在你编译完成之后,就没有任何关系了。

接口和类的加载过程稍微有一点点不同。因为static会触发类的clinit高早起,而接口没有static语句块。但是编译器仍然会为接口生成clinit构造器。一个接口在初始化的时候并不要求其父接口全部完成了初始化。

类的加载过程Class loading

上面那个加载是广义上的加载,具体是加载——验证——准备——解析和初始化这五个步骤。

加载loading

在这个阶段主要要完成3件事情:

  1. 通过类的全限定名字来获取这个类的二进制字节流
  2. 然后把字节流所代表的的静态存储结构转化为方法区(永久代)运行时的数据结构。
  3. 生成一个代表这个类的java.lang.Class对象,这样就可以访问对应的方法区的数据了。

注意这三条都是很宽泛的要求,具体怎么实现完全可以很灵活。比如第一步,我从哪里获得这个class文件呢?可以是从网络中传输过来的(Applet),直接从压缩包里获取(JAR),直接由运算生成(动态代理技术),甚至还可以直接从数据库里获取。

非数组类的加载是比较灵活的,你可以选择重写类加载器的loadClass()方法或者是直接用系统提供的引导类加载器来完成。而数组类就只能由JVM直接创建,但是数组类中的每个元素其实还是需要类加载器来加载这个元素的,而这个元素又有两种可能:

  • 引用类型就用加载过程。
  • 非引用类型(如int),虚拟机会把数组标记为与引导类加载器关联。

加载完成之后,.class文件的内容就被复制了一份到方法区之中。然后在内存中会实例化java.lang.Class类的对象(注意这里说的是内存中,而没有说堆,因为这个对象很特殊)

验证

从这里开始进入连接步骤了。首先问一句,为什么需要进行验证?这是因为class文件可以用任何别的方法来生成,比如可以直接用十六进制编辑器来写class文件,这样就可以写出恶意的代码,一旦恶意的代码没有经过虚拟机检查就直接运行了,后果不堪设想(当然检查也只是为了减少攻击)。但是如果是你自己编写的代码,其实你完全可以-Xverify:none来关掉(因为验证真的很费时间),因为总不至于自己害自己吧。

文件格式验证

首先确认是不是class文件(咖啡宝贝开头),然后看看版本号能不能被虚拟机处理,再看看常量池里有没有问题……总之这一阶段的主要目的是保证字节流能够被正确的解析并且存储在方法区里,而完成了这个步骤之后接下来的验证就都事基于方法区的存储结构了,不再是直接操作字节流了。

元数据验证

主要是对语义进行分析。随便举几个例子:类必须要有父类(除了Object类),不能重载出错等等等。

字节码验证

此步骤确保程序语义是合法的。比如保证跳转指令不会跳转到别的方法体里面。显然这一步其实是非常非常难的,因为其高度复杂。

符号引用验证

符号引用中的全限定名能否找到相应的类,检查一下有没有相对应的字段和方法,各个类和字段的访问性等。

准备

这个阶段是为类变量(注意是类变量,看清楚!)分配内存并设置初始值的,而类变量所用的内存都在方法区开辟。实例变量是和对象一起分配在堆里面的。这一阶段会把所有的变量初始化为零值。比如public static int value = 123;,会把value初始化为0(真正赋值成123是在之后的初始化阶段,不是现在)。有一种情况是有特例的,如果你声明类变量的时候加上了final,那么编译器就会为字段表增加ConstantValue值,这样value就直接等于123而不再变动。

解析

首先需要解释清楚两个概念:

  • 符号引用:用一组符号(字符也是符号,比如a)来描述所引用的目标。最典型的例子就是之前分析class文件的时候,会发现引用了常量池里的某个字符串,那个就是符号引用。
  • 直接引用:被引用的目标必须在内存中已经存在。而直接引用可以是直接指向目标的指针。

其次是解析是有缓存机制的。

解析一个类或接口

假设目前代码所在的类是D,要解析一个从未解析过的符号引用N,将其解析为另外一个类(或者接口)C的直接引用:

  1. 如果C它不是一个数组类型,那么虚拟机首先解析这个符号引用N,然后就得到了全名,并且把全名传递给D的类加载器,让它去加载类C。
  2. 如果C是一个数组类型,那么就需要首先加载数组的元素,然后让虚拟机去生成代表数组维度和元素的数组对象。
字段解析

如果解析一个字段,首先会对字段表里面的class_index中的CONSTANT_Class_info(我知道你已经忘得差不多了,这东西就是在常量池里面)进行解析,如果失败则直接失败,如果成功,那么会对这个字段所属的类的后续的字段进行搜索。

如果这个类本身就含有简单名称和描述符都互相匹配的字段,那么只需要返回这个字段的直接引用即可。没找到的话首先看看这个字段所属的类有没有实现某个接口,然后从下到上开始寻找父接口,同样也是如果找到了简单名称和描述符互相匹配的字段就返回直接引用。然后再看继承关系,和接口一样的找法。如果还是没有,就要抛出NoSuchFieldError异常。但是就算成功找到了,也要进行权限验证,如果不具备对字段的访问权限,那么就会抛出IllegalAccessError异常。

你可能会问要是在父接口和父类中同时出现了同一个字段怎么办?编译器是会拒绝编译的。

方法解析

首先需要解析方法表里面的class_index里面的所属的类或者接口的符号引用。如果发现是个接口,那么就直接抛出IncompatibleClassChangeError异常。通过了的话就能去找是否有简单名称和描述符都匹配的方法,如果有的话就返回直接引用;没有的话就去父类中找,否则再去父接口中找到。如果最后在父接口中找到了,则该类是个抽象类,需要抛出AbstractMethodError异常。

接口方法解析

首先还是在class_index里面找,因为这里是接口了,所以如果发现是个类,则需要抛出IncompatibleClassChangeError异常。然后在找找所属的这个接口中有没有直接匹配的,再去父接口中寻找,最后没找到则抛出NoSuchMethodError异常。

初始化

类初始化是类加载的最后一步了。这里才开始真正执行字节码文件。之前提到过在准备阶段类变量已经赋予过一次零值,初始化阶段则是根据程序员编写的代码来进行初始化。简单讲就是执行clinit()方法(这个方法是类构造器,而不是通常的构造器)的过程。这个方法是编译器自动收集类中的类变量的赋值动作和static语句块(即静态语句块)合并产生的。当然收集的顺序就是出现的顺序决定的,静态语句块比较特殊,它只能访问在它之前的变量,而定义在它后面的变量,它可以赋值但是无法访问。

1
2
3
4
5
6
7
8
9
10
11
public class Test {
static {
i = 0;
}

static int i = 1;

public static void main(String[] args) {
System.out.println(i);
}
}

猜猜这段代码会输出什么?会输出1。这是因为之前也说了,编译器是按照出现的先后顺序(即静态语句块在前,类变量赋值为1在后)的操作来进行收集的,所以当然是后面的会覆盖前面的啦。不知道你会不会有疑问,为什么i=0这句居然不报错呢?因为在准备阶段会为这个类变量进行赋予零值呀。

注意!到这里为止,我们还没有对类的成员变量(也叫实例变量)进行任何的赋值过。

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
static {
i = 0;
System.out.println(i);
}

static int i = 1;

public static void main(String[] args) {
System.out.println(i);
}
}

上面这段代码是非法的,无法通过编译。

clinit方法被称为类构造器,而与类同名的函数叫做实例构造器init(),虚拟机能够保证在子类的clinit方法执行前父类的已经执行完毕。

init is the (or one of the) constructor(s) for the instance, and non-static field initialization.
clinit are the static initialization blocks for the class, and static field initialization.

显然如果一个类没有类变量和static语句块,那么就不需要clinit方法了。下面看一个实际的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
static class Parent {
public static int A = 1;

static {
A = 2;
}
}

static class Sub extends Parent {
public static int B = A;
}

public static void main(String[] args) {
System.out.println(Sub.B);
}

}

因为Parent类的clinit会首先被执行,所以A是2,自然程序的结果也是2。

接口和类相比只是少了static静态块。但是接口的clinit方法不需要先执行父接口的clinit方法,除非父接口中定义的变量需要使用,父接口才会初始化。同样接口实现类在初始化时也不会去执行接口的clinit方法。

虚拟机会保证一个类的clinit在多线程环境下通过加锁被正确初始化,但是这会出现死锁问题。下面的代码演示了这个问题。

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
public class Test {
static class DeadLoopClass {
static {
if (true) {
System.out.println(Thread.currentThread() + "init DeadLoopClass");
while (true) {
// loop forever
}
}
}
}

public static void main(String[] args) {
Runnable script = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + "start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread() + "run over");
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}

上面代码有一个if(true)看上去很沙雕的判断条件,但是其实是至关重要的。

类加载器

比较两个类是否相等,大前提就是加载这两个类的类加载器是否相同,如果连类加载器都不同,那么就没有意义。

双亲委派模型

面试中被问烂的问题之一。

从虚拟机的角度来看,只有两种类加载器:一种是启动类加载器(Bootstrap),这个类加载器本身由C++实现,是虚拟机的一部分;另外的类都归为另一类,全部都由java来实现,并不属于虚拟机,全部继承自java.lang.ClassLoader。当然这个是从虚拟机的角度来看的,面试官想要听到的答案是下面的三层类加载器:

  1. 启动类加载器,Bootstrap Class Loader,加载放在$JAVA_HOME/lib下的类库,而且名字必须要符合。如果你希望能够调用这个类加载器,只需要指明加载器为null即可。
  2. 扩展类加载器,Extension Class Loader,加载放在$JAVA_HOME/lib/ext下的类库,JDK9之后被模块化取代,也没有对应的目录存在了。这个加载器可以直接被使用。
  3. 应用程序类加载器,Application Class Loader,用来加载用户类路径上的所有类库。

双亲委派的意思就是,当类加载器收到加载类的请求的时候,它首先先委派给父类去加载,只有当父类反馈说自己无法加载的时候,自己才会去尝试加载。

模块化系统

JDK9中正式引入的一个模块化系统,但是我还没了解过,所以这部分就暂且先跳过了。

从我开始接触计算机开始,到现在,计算机还是只认识0和1,所以我们写的程序必然要经过一通操作最后变成机器码才可以执行。但是越来越多的程序语言选择了类似java字节码这种和平台无关、和操作系统无关也和机器指令码完全无关的存储格式。

除了平台无关性,还有一点是我之前没有注意到的,它希望能够实现语言无关性:让其他语言运行在java虚拟机之上。这是因为java虚拟机只和字节码文件进行绑定,而不是和java文件进行绑定。所以我完全可以下载一个能够对python进行解释的解释器,让它把我的Python代码变成字节码,然后扔到java虚拟机上去执行。

你可能会奇怪,java是一种强类型的语言,而Python是弱类型的,为什么两者可以同时放到java虚拟机上去运行呢?因为像java的这些关键字,其本质上是多条字节码组合而成的,所以才能够实现如此不可思议的事情。

阅读全文 »

不知道你有没有遇到过下面的这种情况:

我明明按下了退格键,为什么会出现^H啊,明明平时在shell中输入命令按下退格键功能完全正常,为什么在某些情况下它不是把前面的那个字符删除呢?

又或者是如下图所示:

image-20200110141056987

明明只是按下了一个键,突然出来了乱七八糟的一堆东西。这就要从terminal mode中开始讲起。

terminal mode

终端有两种模式,分别叫cooked moderaw mode,从字面看就能理解其意思,一个是处理过的,另外一个是不处理的。所以如果你在raw mode中按下backspace,相当于输入了一个控制字符,但是因为是raw mode,它不会对你输入的控制字符进行处理,但是又不得不显示一下(总不能空着吧,不然这么多控制字符怎么区分呢),然后就会用“脱出字符表示法”来进行表示,就是用^开头的这个,所以当你按下退格键的时候你能看到^H。可以参考这里查看所有的ASCII字符的脱出表示法。

所以总结就是:你在cooked模式下如果分别按下1,2,3,退格键和4这五个按键,那么终端会显示124,但是如果你在raw模式下,则会输出123^H4

如何输入控制字符和可显示字符

我们都知道ASCII字符分成两种,一种是可显示的字符,另一种是控制字符:

  • 可显示的字符不论是在raw模式下还是cooked模式下都是一样的,这些字符全部在键盘上,相信你只要多看几眼键盘一定能打出这些可显示字符。
  • 而控制字符就有点特殊了,因为它本身就有特殊的含义,所以需要特殊处理。如果你想特殊字符发挥作用,那么你只需要根据脱出字符,把前面的^改成ctrl即可。比如^C,那你只需要输入ctrl+c即可输入这个控制字符了。当然你可以通过stty这个命令来修改。而如果你想输入这个字符,你还需要在前面加上一个ctrl+V。可以通过下面的代码来验证。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>       
int main()
{
char ch;
while(1)
{
printf("Enter any character: ");
ch = getchar();
printf("%c is pressed.\n", ch);
printf("ascii is %d\n",ch);
// get "enter"
ch = getchar();
}
return 0;
}

就是你输入一个字符,它会帮你把你输入字符的ASCII码打印出来(已经把回车符去掉了)。那么怎么做到下面图中所示的这样呢?

image-20200110151353126

在ASCII中3号是“ctrl+C”,而要是你在键盘上按下了这个组合键,那么程序就停下来了,自然也不会输出了。所以上面是怎么做到的呢?想想看我们在java中是怎么输出双引号的,我们用了反斜杠来作为脱字符。自然而然,在linux中也有类似的脱字符,是ctrl+V。也就是输入ctrl+V,ctrl+c即可。

应用在实际中,比如我们都知道如果要发送GET的数据包,那需要GET / HTTP/1.0\r\n\r\n,来实际操作看看:

1
2
3
nc -p 1234 www.baidu.com 80
GET / HTTP/1.0
↑此时光标在这里

那我此时如果按下enter,就会发送GET / HTTP/1.0\n出去了,这显然不是我想要的,所以我需要输入\r,而不是让它回车,所以我需要先ctrl+v,然后在输入ctrl+j,这样我就把\r这个字符输进去了,另外同理,就这样输入完成:

image-20200110170453409

最后在输入ctrl+d即可。

PS:最后一个可千万别ctrl+v,然后在ctrl+d哦。这样就把一个数据包发出去了。

PPS:因为现在服务器其实你发送GET / HTTP/1.0\n\n它也会给你答复的,所以其实你按下两个回车也能得到答案,但是这里是为了说明用法。

stty

这个命令能让你修改相关的按键。我们先来看看默认输出吧(ubuntu18.04)

image-20200110163940878

  • intr:发送一个interrupt信号。这就是为什么你按下ctrl+c能够终止程序的执行
  • quit:发送一个 quit信号。
  • erase:清除最后一个输入的字符,功能等同于退格键。
  • kill:把当前这一行整行删除
  • eof:表示这一个文件输入完了。
  • eol:表示这一行输入完了。
  • eol2:eol的备用
  • swtch:切换到另外一个shell(存疑)
  • start:在停止标准输出后再度启用它。
  • stop:停止标准输出。按下之后屏幕就被“冻结”了
  • susp:发送一个terminal信号。
  • rprnt:重新绘制当前的行。
  • werase:清除最后一个单词,退格键的加强版。
  • lnext:转义符的定义。
  • discard:toggle discarding of output…我翻译不出来

后面还有好几行,有兴趣的自己man stty看吧。

最早接触到这个命令是在18年12月,和同学聊天的时候聊到了这个命令,但是我不知道这个命令,被他一通鄙视…当时就去简单看了一下,现在过了一年了,打算在这篇博客里面好好整理一下。

总体介绍

ncnetcat 的缩写, 它是类unix系统下一个功能强大的命令行网络工具,被用来在两台主机之间建立TCP或者 UDP连接,并能够通过标准输入输出进行读和写,日常可以配合管道符来进行重定向使用。

我在我的ubuntu下面看到nc其实是一个软连接,分别是/bin/nc -> /etc/alternatives/nc -> /bin/nc.openbsd,而在centos下面则是/usr/bin/nc -> /etc/alternatives/nmap -> /usr/bin/ncat,但是不论在哪里,你都可以使用nc来执行。

阅读全文 »