Java的异常

异常也是对象,而对象都用类来定义。异常类的根类是java.lang.Throwable。

抛出的异常都是这个图中给出的类的实例,或者是这些类的子类的实例。

我们也可以定义自己的异常类。

Throwable类是所有异常类的根。所有异常类都直接或间接地继承自Throwable。这些异常可以分为三种类型:系统错误、异常和运行时异常。

RuntimeException和Error都称为免检异常(unchecked exception)。所有的其他异常都称为必检异常(checked exception),表示编译器会强制程序员检查并通过try-catch块处理它们,或在方法头处理它们。

Java语言不强制要求编写代码捕获或声明免检异常。

异常的处理器是通过从当前的方法开始,沿着方法调用链,按照异常的反向传
播方向找到的。

Java的异常处理模型基于三种操作:声明异常、抛出异常和捕获异常。

Java对面向对象的语言。在Java中,当前执行的语句必须属于某个方法。Java解释器调用main方法开始执行一个程序。每个方法都必须声明它可能抛出的必检异常的类型。这称为声明异常。这样方法的调用者会被告知有异常。

处理异常的这段代码称为异常处理器(exception handler)

可以从当前的方法开始,沿着方法调用链,按照异常的反向传播方向找到这个处理器。如果调用链找不到处理器,程序就会终止并且在控制台上打印出错信息。寻找处理器的过程称为捕获一个异常。

JDK7可以使用同样的处理代码处理多个异常的情况。

catch(exception1 | exception2 | exception3){ //code }

何时使用异常?

异常处理可以分离正常的程序和错误处理程序。异常处理需要初始化新的对象,需要从调用栈返回,还要沿着方法调用链来传播异常以便找到它的异常处理程序,所以异常处理通常需要花费更多的时间。如果想让该方法的调用者处理异常,应该创建一个异常对象并将其抛出。

当必须处理不可预料的错误状况时应该使用try-catch块。不要用异常处理来处理简单、可预料的情况。不要把异常处理当作逻辑判断。

允许异常处理器再抛出异常。这样是为了让调用者注意到这个异常。

在异常处理器中同原始异常一起再抛出一个新的异常叫链式异常(chained exception)。

自定义异常最好不要继承免检异常。继承必检异常可以让编译器在程序中强制捕获这些异常。

SpringBoot项目的目录结构及其作用

servicex                 // 项目主文件夹(用项目名称命名)
    |- admin-ui          // 管理服务前端代码(一般将UI和SERVICE放到一个工程中,便于管理)
    |- servicex-auth     // 模块1
    |- servicex-common   // 模块2
    |- servicex-gateway  // 模块3
    |- servicex-system   // 模块4
        |- src
            |- main                  // 业务逻辑
                |- assembly          // 基于maven assembly插件的服务化打包方案
                    |- bin           // 模块脚本(启动、停止、重启)
                    |- sbin          // 管理员角色使用的脚本(环境检查、系统检测等等)
                    |- assembly.xml  // 配置文件
                |- java              // 源码
                    |- com
                        |- hadoopx
                            |- servicex
                                |- system
                                    |- annotation     // 注解
                                    |- aspect         // 面向切面编程
                                    |- config         // 配置文件POJO
                                    |- filter         // 过滤器
                                    |- constant       // 存放常量
                                    |- utils          // 工具
                                    |- exception      // 异常
                                    |- controller     // 控制层(将请求通过URL匹配,分配到不同的接收器/方法进行处理,然后返回结果)
                                    |- service        // 服务层接口
                                        |- impl       // 服务层实现
                                    |- mapper/repository // 数据访问层,与数据库交互为service提供接口
                                    |- entity/domain     // 实体对象
                                        |- dto // 持久层需要的实体对象(用于服务层与持久层之间的数据传输对象)
                                        |- vo // 视图层需要的实体对象(用于服务层与视图层之间的数据传输对象)
                                    |- *Application.java  // 入口启动类
                |- resources         // 资源
                    |- static        // 静态资源(html、css、js、图片等)
                    |- templates     // 视图模板(jsp、thymeleaf等)
                    |- mapper        // 存放数据访问层对应的XML配置
                        |- *Mapper.xml
                        |- ...
                    |- application.yml        // 公共配置
                    |- application-dev.yml    // 开发环境配置
                    |- application-prod.yml   // 生产环境配置
                    |- banner.txt    
                    |- logback.xml            // 日志配置
            |- test                  // 测试源码
               |- java               
                    |- com
                        |- hadoopx
                            |- servicex
                                |- system
                                    |- 根据具体情况按源码目录结构存放编写的测试用例
        |- target     // 编译打包输出目录(自动生成,不需要创建)
        |- pom.xml    // 该模块的POM文件
    |- sql            // 项目需要的SQL脚本
    |- doc            // 精简版的开发、运维手册
    |- .gitignore     // 哪些文件不用传到版本管控工具中
    |- pom.xml        // 工程总POM文件
    |- README.md      // 自述文件

C和指针

在C中,我们可以使用指针来访问数据。

完整的说法叫,使用间接访问操作符(也就是*号)进行间接访问。这个指针也叫解引用指针。

所有在内存中的数据无非是0和1,具体表示什么含义需要我们去定义。内存中的值可以表示整数、可以表示浮点数、字符串、甚至是另一个内存的地址。

而例如“int *”的指针用法,表示这个内存存储的值是一个内存地址,而这个内存地址存储的值表示整形。

&表示取地址操作。

*&a=25 和 a=25 的结果是相同的,都是将25赋值给了a。但前者涉及了更多的操作。在不同的编译器(或优化器)的情况下, 如果没舍弃其中多余的操作,产生的机器码可能更慢,更大。而且源码的可读性更差。

对于字面量,需要进行类型转换。*(int *)100=25,这语句表示将25赋值给地址为100的内存。强制转换的过程叫做指向整形的指针。而100是字面量,默认为整形。这种使用方法通常在嵌入式编程中,因为你无法指定内存位置100的内容是什么。而嵌入式编程中,通常可以指定内存存储的东西。

对于两种声明方式,int* a和int *a,有两种看法,第一种是可以看作指向整形的指针,第二种可以看作整形的变量,这个变量代表地址。

C函数的所有调用均是“传值调用”。例如数组名传递的是地址,而地址也是值。函数传递的只是一份指针的值。

C语言的一些“约定”

在C语言中以“#”开头的为预处理指令。在预处理阶段,由预处理器处理。这部分不属于C代码中被执行的一部分。通常是告诉预处理器这里应该如何处理源码。


防止重复引用头文件。

#pragma once

但#pragam是编译器相关的,它的设定的状态,或指示编译器完成一些特定的动作。如果编译器没有实现功能可能会编译报错。

也可以使用编译器无关的宏定义方法:

// 文件名为:GateWay_private.h
#ifndef RTW_HEADER_GateWay_private_h_
#define RTW_HEADER_GateWay_private_h_

// 头文件

// 主要代码

#endif                                 /* RTW_HEADER_GateWay_private_h_ */

其中宏定义的名称没有特定的要求,只要在工程中命名唯一就行。可以根据公司规定、项目约束和个人习惯来设置。可以使用“项目+文件名”的方式,例如RTW_HEADER表示RTW工程的头文件,GateWay_private_h_为该文件的文件名。并在最后给出结束位置的提示/* RTW_HEADER_GateWay_private_h_ */

也可以直接使用文件名,只要命名唯一即可,但这种方式通常用来公用的头文件上,例如变量类型的定义:

// 文件名为:GateWay_private.h
#ifndef GATEWAY_PRIVATE_H
#define GATEWAY_PRIVATE_H

// 引用头文件

// 主要代码

#endif // !GATEWAY_PRIVATE_H

为了在C++代码中调用C写成的库文件,就需要用extern”C”来告诉编译器:这是一个用C写成的库文件,请用C的方式来链接它们。

extern “C” 表示编译生成的内部符号名使用C约定。

#ifdef  __cplusplus
extern "C" {
#endif

// 引用头文件

// 主要C代码

#ifdef  __cplusplus
}
#endif

这段话的上下文意思是,当C++程序调用C语言程序时,需要使用C语言的编译方式来处理这段代码。

因为C++支持函数重载,而C不支持函数重载。两者语言的编译规则不一样。编译器对函数名的处理方法也不一样。

void func(int a,int b)
{
  //code  
}

针对上面这个函数,C编译之后,可能为_func,而C++编译之后会产生_func_int_int之类的名字(不同的编译器可能生成的名字不 同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。

如果不区分的话,之后的链接会出现找不到具体函数的错误,会导致链接失败。

一般我们都将函数声明放在头文件,当我们的函数既有可能C使用,也有可能被C++使用时,我们无法确定是否要将函数声明在extern “C”里,所以,我们应该使用上面提到的#ifdef __cplusplus方式。


为什么一个字节是8个比特?

为什么一个字节byte是8个bit呢?而不是6个或10个或其他数量呢?

往前追溯,byte是IBM公司在1956年提出的概念,原本叫做bite,但为了不和bit混淆,改为byte。

ASCII编码于1967年提出。

在那时候,开发一个东西,肯定是从简单开始,也就是最接近某一类人能使用的情况下开发的。这里自然指的是使用英语的人。而英语只有26个字母,加上大小写,8个比特位足以。基于简单的下,就使用了8个比特位来使用。

比ASCII更简单的还有BCD码(Binary-Coded Decimal‎)。用4位二进制数来表示1位十进制数中的0~9这10个数码。是一种二进制的数字编码形式,用二进制编码的十进制代码。

在当时8个比特位已经可以表示很多东西。

字节的单位符号被指定为大写的“B”,1B表示的是1byte

参考:Byte – Wikipedia

C语言的头文件

引入编译器自带的头文件(包括标准头文件)用尖括号。

引入程序自定义的头文件用双引号。

#include <stdio.h>  //引入标准头文件
#include "file.h"  //引入自定义的头文件

使用尖括号,编译器会到指定系统路径下查找头文件。

使用双引号,编译器会根据引入的当前路径查找头文件,如果没有找到,再到系统路径下查找。

系统路径是默认路径,在Windows下这个路径由编译器自己携带。在Linux下这个路径一般固定在某个位置。

其实无论是尖括号还是双引号,都可以用来包含头文件。只要路径正确就行。

#include "D:\\abc\xyz.h"
#include "xyz.h"
#include "include/xyz.h"
#include "../xyz.h"
#include "../include/xyz.h"

可以使用绝对路径来查找头文件。也可以使用相对路径。

通常头文件只写一个文件名,但其实这是./的缩写

#include "./xyz.h"

在window下,文件系统的分隔符可以是正斜杠/,也可以是反斜杠\。无论正斜杠还是反斜杠都可以有一个也可以有两个。

在Linux下,文件系统分隔符只能是一个正斜杠。

C函数的参数传递

C函数的参数分为实参和形参。

总的来说,实参和形参是分开的,形参的值无论如何都不会改变实参值。但可以通过指针改变指针指向的内存地址的值。

C语言采用的是值传递,无论形参如何变化,都不会改变实参的值。如果参数是指针,改变的是指针所指的地址中的内容。但参数的指针无论如何变化,都不会改变。

首先来看普通变量的参数传递。

void func(int arg) {
	arg += 1;
}

void main(){
	int a = 0;
	func(a); // a的值为0
}

变量a初始化为0,执行func函数后依旧为0。因此在普通变量下实参和形参不影响。

那如果数组变量呢?

void func(int arg[]) {
	arg[0] += 1;
	arg[1] += 1;
}

void main(){
	int a[] = { 0, 1};
	func(a); // 数组a的值变成了{1, 2}
}

为什么数组的值改变了呢?因为数组本身是指针变量。数组名表示的是数组的首地址,而数组后面的下标表示数组地址上的值。我们调用函数时传递的是数组的首地址,而函数改变的是这个地址上后续的值。

void func(int arg[]) {
	arg += 1;
}

void main(){
	int a[] = { 0, 1};
	func(a); // 数组a的首地址保持不变
}

上面的代码执行后a的首地址保持不变。其中有趣的地方在于形参。

我们知道,变量的地址是无法通过赋值改变的,但指针的地址可以。下面的代码编译器是无法通过的,因为赋值语句在尝试修改数组的首地址,这是不允许的。

void main(){
	int a[] = { 0 };
	a += 1; // 编译器无法通过
}

但上面的代码中,函数func拥有形参数组a,而在函数体中,修改了数组的首地址,这语句编译器通过了。说明形参“int a[]”的本质不是数组,而是指向数组首地址的指针。

因为指针变量允许修改变量中的地址值。在不用const修饰的情况下,也允许修饰地址指向的内存的值。

对于数组来说,可以把形参都看成临时的指针变量,无论如何修改指针的变量,改变的只是当前指针的值。而不会改变指针指向地址的值。也就是不会改变数组的值。

只有修改指针变量指向内存地址的值,实参才会改变。


既然数组的本质是地址,那形参作为指针变量也很容易理解了。

void func(int* arg) {
	arg += 1;
}

void main(){
	int a = 0;
	func(&a); // a的值依旧是0
}

但如果使用的是星号,那变量a的值会发生改变。

void func(int* arg) {
	*arg += 1;
}

void main(){
	int a = 0;
	func(&a); // a的值变为了1
}

C语言中const关键字

C语言中的const关键字表示这个变量的值在其整个作用域中都保持不变。

但const是在语义层面的检查,是编译器在编译期间,用语法检测来保证数据没有被修改。但在运行时,const修饰的变量和其他未被const修饰的普通变量一样是放在内存中的数据区。必要时可以通过修改内存来达到修改值的目的。

void main(){
	int a = 0;
	a = 1;
}

一个变量有两个维度来描述,一个是地址,另一个是这个地址上的值。上面这段代码,我们在变量a的地址上修改了它的值,由0修改为了1。

void main(){
	const int a = 0;
	a = 1; //编译时不允许通过
}

上面的代码结果是显而易见的,编译无法通过。因为代码虽然没有尝试修改a的地址,但修改了a的值。

既然const关键字限制了变量值的修改,那变量的地址呢?

void main(){
	int a = 0;
	&a = &a; // 编译器不允许通过
}

变量的地址本身是无法修改的,因为同样不被编译器允许,即使这个地址就是它自身的地址。


既然const修饰的普通变量值和地址都无法修改,那数组变量值修改呢?我们先来看没有const修饰的数组。

void main(){
	int a[] = { 0, 0 };
	a[0] = 1;
	a[1] = 1;
}

对于数组也需要分为两个维度,一个是数组的首地址,另一个是数组的值。对数组来说,数组名表示的是数组的首地址,而数组的值和数组的数据类型有关,但最终也表示数值。

void main(){
	const int a[] = { 0, 0 };
	a[0] = 1; // 编译时不允许通过
	a[1] = 1; // 编译时不允许通过
}

上面的数组用const修饰后,结果也很明显,编译无法通过。因为我们无法修改数组的值。

而对于数组的地址,自然也是无法编译通过。

void main(){
	int a[] = { 0, 0 };
	int b[] = { 1, 1 };
	a = b; // 编译无法通过
}

因为数组名代表的就是数组的首地址,“a = b”这条语句相当于把数组b的地址赋值给了数组a 的地址。而普通变量代表的是这个地址上的值,普通变量名的赋值操作是值对值,而数组名的赋值操作时地址对地址。

void main(){
	int* a = 0;
	int b[] = { 1, 1 };
	a = b; // a的值为数组b的地址
}

由于编译器的原因,这其中的细节被隐藏掉了,导致看起来不太习惯。这也是我们对数组初始化只有用for循环或内存操作memset()的原因。

说回const的话题。如果数组存放的是指针呢?能否修改其中的值?

void main(){
	const int* a[] = { 0 };
	int b = 0;
	a[0] = &b;
}

上面的代码是可以通过编译的,最终a[0]的值为整型变量b的地址。但如果尝试修改b的值呢?

void main(){
	const int* a[] = { 0 };
	int b = 0;
	a[0] = &b;
	*a[0] = 1; // 编译器不允许通过
}

上面这段代码编译器不允许通过,因为我们尝试修改了指向整型变量的地址上的值。我们回到普通的变量来看针对指针变量的情况。


首先我们要知道,变量的地址本身是无法修改的,因为同样不被编译器允许。

void main(){
	int a = 0;
	int b = 0;
	&a = &b; // 编译器不允许通过
}

既然const修饰的普通变量的值我们无法修改,那指针变量呢?

void main(){
	const int a = 0;
	int b = 0;
	a = &b; // 编译器不允许通过
}

上面这段代码很清晰,const修饰的整型变量a的值不允许修改,因此将b的地址赋值给a的操作是不被允许的,编译器自然无法通过。

但如果a是指向整型变量的指针类型呢?例如下面这段代码。

void main(){
	const int* a = 0;
	int b = 0;
	a = &b;
}

很奇怪的是,编译器通过了。指针变量a表示的是,这个变量存放着指向整型变量的地址。但这个地址的值是允许改变的。

那什么情况下不允许改变呢?

void main(){
	const int* a = 0;
	int b = 0;
	a = &b;
	*a = 1; // 编译器不允许通过
}

当我们尝试去修改指针变量a所指向的地址的值时,这个情况是不允许的。上面的代码相当于尝试修改变量b的值,但被编译器拒绝了。

当没有const修饰时,b的值可以被正确修改。

void main(){
	int* a = 0;
	int b = 0;
	a = &b;
	*a = 1; // 变量b的值被修改为了1
}

由此看来。const并不是完全限制当前变量的值的修改。

而是,当const修饰的变量是普通变量时,不允许修改其中的值。当const修饰的是指针变量时,不允许修改这个指针指向的地址上的值。

而针对多级指针,也是如此。

void main(){
	const int** a = 0; // 二级指针
	int* b = 0;
	int c = 0;
	a = &b;
	b = &c;
	** a = 1; // 编译不允许通过
}

上面二级指针变量a指向了b,b指向了c。如果没有const修饰,最终变量c的值为1。但当有const修饰时,编译器不允许代码修改最终的变量的值。

开发软件环境和工具

下载软件

Free Download Manager,免费。Free Download Manager – 從網路下載任何東西

文件对比

DiffMerge,免费,但功能和界面比较单一。支持Windows、OS X、and Linux。SourceGear | DiffMerge

Beyond Compare,收费,但可以永久使用。功能强大,适用于绝大多数场景。Scooter Software: Home of Beyond Compare

UItraCompare,收费,且按年收费。功能强大。Compare Files, Folders, Text | UltraCompare (ultraedit.com)

反编译器

ILSpy,.Net反编译器,开源免费。icsharpcode/ILSpy: .NET Decompiler with support for PDB generation, ReadyToRun, Metadata (&more) – cross-platform! (github.com)

jclasslib bytecode viewer,字节码查看软件。开源免费。ingokegel/jclasslib: jclasslib bytecode editor is a tool that visualizes all aspects of compiled Java class files and the contained bytecode. (github.com)

API 接口测试

Postman:有免费版,也有付费版。https://www.postman.com/downloads/

Postcat,开源免费,并且可以在线测试:https://github.com/Postcatlab/postcat

小工具

Md5Checker,MD5比较工具,免费。Md5Checker (getmd5checker.com)

Mouse without Borders,微软的免费软件。多主机、多屏幕控制,单个鼠标和键盘控制多达四台计算机。Download Microsoft Garage Mouse without Borders from Official Microsoft Download Center

Notepad3,Windows记事本增强工具,免费开源。Download Notepad3 – Notepad replacement with syntax highlighting. (rizonesoft.com)

Snipaste,桌面截图工具。轻量、免费、开源。Snipaste官网github仓库

Listary,文件搜索增强,免费。但好像开机启动时会进行扫盘操作,会导致磁盘占用较高。Listary – File Search & App Launcher

魔法道具

Pigcha,魔法跃迁道具,有流量收费,也有包月,价格优惠。Pigcha官方地址

M考拉,魔法跃迁道具,有免费也有收费,价格优惠。M考拉 | Secured Private Networks (kao-la.best)

EasyConnect,深信服产品,EasyConnect官网

Java开发工具

Eclipse,开源免费。Eclipse官网

MyEclipse,免费试用30天,35$/年。MyEclipse官网

IDEA,有免费的社区版,IDEA官网

Redis可视化工具

QuickRedis,国人开源、免费的Redis可视化管理工具。官网:QuickRedis官网,开源地址:QuickRedis开源地址

Another Redis Desktop Manager:开源的Redis可视化工具,比QuickRedis的体验更好。开源地址:Another Redis Desktop Manager开源地址

Linux

VMware Workstation Player,免费的Linux虚拟机工具。VMware Workstation Pro是收费的。VMware Workstation Player | VMware | CN

版本控制

Sourcetree,免费。Sourcetree | Free Git GUI for Mac and Windows (sourcetreeapp.com)

TortoiseSVN,免费。Home · TortoiseSVN

git,免费。Git (git-scm.com)

数据库设计

Open System Architect,免费软件,Open System Architect官网

Mysql Workbench,免费软件,同时可以负责数据库。

其它

Potplayer,免费且强大的视频播放器,Global Potplayer

geek,用来删除程序的软件,有免费也有收费版本,收费版本是终身制。geek官网

Bandizip,解压缩软件,分为Windows版本和Mac版本,免费但稍微有广告,也有永久许可证的收费版本。bandisoft官网

7-Zip,开源解压缩软件,7-Zip网站

NanaZip,开源解压缩软件,在7-Zip的基础上新增了符合现代操作系统的界面。NanaZip网站

计算机性能工具

CPU-Z,免费软件,可以收集系统的主要设备信息,CPU-Z | Softwares | CPUID

CrystalDiskMark,免费软件,免费的磁盘测试软件,Crystal Dew World [en] – (crystalmark.info)

DiskGenius,有免费版本,也可以升级为付费版本。系统迁移和硬盘分区软件。Free Download DiskGenius Online

Fritz Chess Benchmark,免费软件,用于测试CPU性能。Fritz Chess Benchmark – Download (updatestar.com)

序列化和反序列化

什么是序列化和反序列化呢?

序列化就是将对象转成字节序列的过程。

反序列化就是将字节序列重组成对象的过程。

为什么要有对象序列化机制

程序中的对象,都是存放在内存中的。当我们关闭程序或内存掉电后,无论如何它都不会继续存在了。那么有没有一种机制能让对象具有“持久性”呢?

序列化机制提供了一种方法,你可以将对象序列化的字节流输入到文件,并保存在磁盘或数据库上。

序列化机制的另外一个应用场景是,我们可以通过网络传输对象。Java中的远程方法调用(RMI),底层就需要序列化机制的保证。

序列化机制从某种意义上,也弥补了不同平台带来的差异。毕竟转换后的字节流可以在其他平台上进行反序列化来恢复对象。


Java中的序列化和反序列化

假设我们需要将一个Student类的对象进行序列化。无论是用于持久化还说网络传输都可以。

import java.io.Serializable;

public class StudentClass implements Serializable {

    //...类属性和方法。

}

需要被序列化的类都必须实现Serializable接口。但Serializable接口实际上是一个空接口,里面并没有包含任何接口方法。但这并不代表Student类实现Serializable接口是没必要的。

在这里Serializable起到一个标记作用。它告诉底层代码,该类是可以被序列化的。但真正序列化的代码并不是由Serializable完成。

如果对一个没有实现Serializable接口的类进行序列化会抛出java.io.NotSerializableException异常。

还有一个非常重要的是就是序列化ID:serialVersionUID。

在定义一个可序列化的类时,如果没有显式地定义一个serialVersionUID字段,则Java运行时环境会根据该类的各方面信息自动地为它生成一个默认的serialVersionUID。一旦更改了类的结构或信息,则类的serialVersionUID也会跟着变化。

在反序列化时,JVM会把字节流中的serialVersionUID和被序列化类中的serialVersionUID进行对比。只有两者一致,才能重写反序列化。否则就会报java.io.InvalidClassException异常。

因此,为了serialVersionUID的确定性。建议写代码时,凡是实现了Serializable的可序列化类,都最好显式地声明一个明确的serialVersionUID值。

import java.io.Serializable;

public class StudentClass implements Serializable {

    private static final long serialVersionUID = -4963266899668807475L;

    //...类属性和方法。

}

如果不想手动赋值,可以使用IDE的自动添加功能。例如IDEA可以参考:IDEA如何自动生成serialVersionUID

其中还有两种特殊情况:

  • 凡是被static修饰的字段是不会被序列化的。
  • 凡是被transient修饰的字段也是不会被序列化的。

参考:

7000字带你死磕Java I/O流知识

序列化/反序列化,我忍你很久了