Java 概要
Java 概要
Java环境搭建
开始学习Java , 往往需要配置Java的基础环境, 环境配置好后就可以编写Java代码并编译运行. 可以选择用系统自带的记事本打开编写代码, 用JDK编译运行, 但是我使用的是Idea作为Java的IDE工具进行编译并运行代码
1. Java基础环境搭建
所谓的Java基础环境搭建简单来说就是下载对应的JDK, 安装后配置对应的环境变量, 下面是基础环境搭建的详细流程.
- 首先通过官网下载对应的JDK版本, 个人推荐使用JDK1.8 , 原因是很多教程针对JDK1.8, 我使用的是JDK13
-
下载完成后进行”傻瓜式”安装, 安装完成后检查或配置环境变量, 在计算机中找到环境变量设置的地方, 试着添加如下变量名和值
(福利推荐:阿里云、腾讯云、华为云服务器最新限时优惠活动,云服务器1核2G仅88元/年、2核4G仅698元/3年,点击这里立即抢购>>>)
变量名 值 JAVA_HOME JAVA安装目录 CLASSPATH %JAVA_HOME%libdt.jar CLASSPATH %JAVA_HOME%libtools.jar Path %JAVA_HOME%bin Path %JAVA_HOMT%jrebin -
JDK基础环境安装完毕, 下面通过命令行检测是否安装正确. 打开命令行, 输入
java -version
, 输出结果类似java version "15.0.2" 2021-01-19 Java(TM) SE Runtime Environment (build 15.0.2+7-27) Java HotSpot(TM) 64-Bit Server VM (build 15.0.2+7-27, mixed mode, sharing)
2. Idea安装
IDE是每个从事编程工作的人必须接触的工具, 一个好的IDE可以大大地提高研发效率, Idea就是这样的一款工具.
Idea的安装非常简单, 通过如下几步即可轻松完成:
1. 通过搜索找到相关下载地址或者[官网](https://www.jetbrains.com/idea/download/)下载, 尽量选择**Ultimate**版本下载 2. 下载后的文件如为: `ideaIU-2021.2.1.win.zip`, 解压此文件到想安装的目录即可 3. 运行其中的`idea.exe`, 按照相关指引填写信息后即可进入程序
3. 第一个Java程序
Java的基础环境与编译运行环境已经准备妥当, 下面将运行第一个程序”Hello World!”
- 在idea中依次点击
New Project -> Java -> Next -> Next -> (填写Project相关信息后) -> Finish
, 建议project name设置为HelloWorld - 如果左侧没有Project菜单, 可以点击左侧Project呼出, 之后右键点击
HelloWorldsrc
, 依次选择New -> Java Class
, 添加Name为HelloWorld -
现在, 已经创建了第一个Java的类 HelloWorld, 在此类中写如下内容
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World!"); } }
- 在此文件上右键点击, 选择
Run HelloWorld.main()
, 可在下方Console窗格中看到输出: “Hello World!”至此, 一个最简单的Java程序编写完成. 如果在编写代码中出现错误, 可以在文件中鼠标指向错误标记处点击红色灯泡查看建议. 这也是直接使用IDE的诸多好处之一
基本类型与运算
1. 基本类型概述
Java常用的类型包含表示真假的boolean, 表示字符的char, 表示数值的type, short, int, long, float, double, 表示空的 void, 详细见下
基本类型 | 大小(bit) | 最小值 | 最大值 | 包装器类型 | 默认值 |
---|---|---|---|---|---|
boolean | – | – | – | Boolean | false |
char | 16 | Unicode 0 | Unicode $2^{16}-1$ | Character | ‘u0000′(null) |
byte | 8 | -128 | +127 | Byte | byte(0) |
short | 16 | $-2^{15}$ | $+2^{15}-1$ | Short | short(0) |
int | 32 | $-2^{31}$ | $+2^{31}-1$ | Integer | 0 |
long | 64 | $-2^{63}$ | $-2^{63}-1$ | Long | 0L |
float | 32 | $IEEE_{754}$ | $IEEE_{754}$ | Float | 0.0f |
double | 64 | $IEEE_{754}$ | $IEEE_{754}$ | Double | 0.0d |
void | – | – | – | Void | – |
2. 操作符
操作符用于进行变量或者对象之间的计算或关系判断, 没有操作符就无法做任何运算. 比较 或者赋值. 操作符主要分为以下几类, 分别是: 算术操作符, 赋值操作符. 关系操作符, 逻辑操作符, 位操作符和其他操作符.
-
算术操作符
描述 操作符 变量 demo 结果 加, 减, 乘, 除 +, -, *, / a = 1, b = 2 “a + b = ” + (a + b) a + b = 3 取模 % a = 3, b = 2 “a % b = ” + (a % b) a % b = 1 自增自减 ++, — a = 1 a++; “a++ =” + a .sout a++ = 2 其中, 二元运算可以与等号进行缩写, 比如
a= a+b
可以缩写为a += b
- 赋值操作符
从上面可以看到一个符号=
, 它的目的是把右边的值赋值给左边, 有些书把+=
,-=
等也归入赋值运算符, 窃认为这更倾向于算术操作符与赋值操作符的缩写 -
关系操作符
主要包含六种, 见下表:操作符 描述 == 检查左右两侧操作数是否相等, 相等为真 != 检查左右两侧操作数是否不等, 不等为真 > 检查左侧操作数是否大于右侧操作数, 大于为真 < 检查左侧操作数是否小于右侧操作数, 小于为真 >= 检查左侧操作数是否不小于右侧操作数, 不小于为真 <= 检查左侧操作数是否不大于右侧操作数, 不大于为真 - 逻辑操作符
包含与操作符&&
与或操作符||
-
位操作符
假设下表中的变量i与变量j分别为5和7, 类型为int
操作符 描述 demo 结果 & 左右两个操作数按位进行且操作 i&j 5 左右两个操作数按位进行或操作 i j 7 ^ 左右两个操作数按位进行异或操作 i^j 2 ~ 对操作数按位取反 ~i -6 << 按位向左移动 i<<1 10 >> 按位向右移动 i>>1 2 -
其他操作符
操作符 描述 demo 结果 三目运算符 通过第一个操作数判断条件是否为真从而在后两个操作数中执行一个 1==2?”1==2″:”1!=2″ 1!=2 字符串操作符 前面的例子已经使用了很多次此操作符, 字符串的连接可以通过+或+=实现, 其他类型的与字符串进行+操作时会先转化为字符串 “str ? ” + true str ? true
3. 优先级与结合性
优先级 | 操作符 | 类型 | 结合性 | ||
---|---|---|---|---|---|
1 | () | 括号操作符 | 由左至右 | ||
1 | [] | 方括号操作符 | 由左至右 | ||
2 | !, +(正号), -(负号) | 一元操作符 | 由右至左 | ||
2 | ~ | 位操作符 | 由右至左 | ||
2 | ++, — | 自增自减操作符 | 由右至左 | ||
3 | *, /, % | 算术操作符 | 由左至右 | ||
4 | +. – | 算术操作符 | 由左至右 | ||
5 | <<, >> | 位操作符 | 由左至右 | ||
6 | >, >=, <, <= | 关系操作符 | 由左至右 | ||
7 | ==, != | 关系操作符 | 由左至右 | ||
8 | & | 位操作符 | 由左至右 | ||
9 | ^ | 位操作符 | 由左至右 | ||
10 | 位操作符 | 由左至右 | |||
11 | && | 逻辑操作符 | 由左至右 | ||
12 | 逻辑操作符 | 由左至右 | |||
13 | ?: | 条件操作符 | 由右至左 | ||
14 | = | 赋值操作符 | 由右至左 |
流程控制
程序在执行时会出现各种情况, 例如之前通过关系操作符和逻辑操作符得出的结果, 是否应该走向不同的程序分支, 至于分支的实现就属于流程控制. 另外, 程序可能会出现不断执行某语句知道某些条件不成立为止的情况, 这也属于流程控制. Java处理流程控制的关键词和语句包含if-else, while, do-while, for, return, break, continue, switch
1. if-else
if-else语句主要时依据if语句的判断结果, 选择不同的分支路径. 此语句有一些其他的写法, 比如if不带else, 或者else后面可以再连接一个if语句继续进行条件判断
if (num < 10) { "num < 10".sout } if(num < 100) { "num < 100".sout } else { "num >= 100".sout } if(num < 50) { "num < 50".sout } else if (num < 100) { "num >= 50 and num < 100".sout } else { "num > 100".sout }
2. switch
当使用if-else语句时. 如果判断的条件过多, 可能会出现大量的if-else语句, 这样的代码可读性应该时很差的. 这种情况可是选择使用switch语句, switch给出所有待选条件, 当符合条件判断时将开始执行
switch(num){ case 1: "num is 1".sout break; case 2: "num is 2".sout break; case 3: "num is 3".sout break; default: break; }
switch主要写法如上所示, 如果去掉case后面的break语句, 那么将会一直向下执行直到执行break或结束. 连续执行的特性在实际使用时会有用处, 但是在没有彻底搞清楚前不建议使用
3. for
for循环需要依靠三个字段(初始值, 结束条件, 游标移动)来达成循环的目的, 但是也有(类型 循环变量:变量)的形式
int[] arr = new int[10]; for(int i = 0; i < 10; ++ i) { arr[i] = i; } for(int i : arr){ i.sout }
4. while/do-while
while也是一个循环控制的方法, while后面跟随一个判断条件, 当条件成立时则立即执行后面程序段的语句, do-while则是先执行程序段的语句再进行判断
int[] arr = new int[10]; int i = 0; while(i < arr.length){ arr[i] = i; i ++; } int j = 0; do { arr[j].sout j ++; } while(j < arr.length)
5. break与continue
break与continue在循环中起到非常重要的作用. break可以直接退出整个循环, 当多层嵌套时, 仅退出break所在的循环, continue可以结束本次循环, 当多层嵌套时, 仅结束continue所在的循环
int[] arr = new int[10]; for(int i = 0; i < 10; ++ i) { arr[i] = i; } for(int i : arr){ if(i == 3) { continue; } if(i == 6) { break; } i.sout }
6. return
return可以直接退出当前的方法, 并且可以带出返回值(除void)
如果一个void方法没有写return , 那么可以理解为该方法的最后有一个隐式的return
return后面的语句一般不会执行, 但是有一个例外(finally), 在下面的异常讲解时会进行讲解
对象
java是一种面向对象的语言, 什么是面向对象以及如何使用对象?
1. 什么是对象
面向对象的核心就是把任何事物抽象为类, 这个事物具备的能力就是抽象出来的方法, 这个事物具备的各个实际物品就是抽象出来的字段. 比如手机, 把手机比喻为对象, 那么手机的硬件(比如CPU, 显示屏)就是对象里的字段, 打电话, 上网等就是抽象出来的方法.以学生为例:
public class Student { private int age; /* 学生的年龄 */ private String name; /* 学生的姓名 */ public int getAge() { return age; } public String getName() { return name; } public void setAge(int age) { this.age = age; } public void setName(String Name) { this.name = name; }}/** 创建实体: Student student = new Student();*/
2. 什么是方法
方法主要包含四个内容, 分别是: 返回值, 方法名, 参数, 方法体. 同时, 也可使用其他关键字来修饰一个方法以达到其他能力.
普通方法的调用格式是Object.fun(args); 比如: student.getAge()
3. this与static
上面的例子中出现了this.age = age
, 这里this的作用是代指调用这个方法的实例, 普通成员方法都是默认有this的, 其意义一般是指代调用的实例;
static修饰的方法为是静态方法, 静态方法无法使用this指代调用, 它只对所属的类负责.
4. 访问权限
权限名称 | 关键词 | 权限范围 | 用法 |
---|---|---|---|
公开访问权限 | public | 所有都可访问 | 一般用于希望别人使用的方法或公开的api |
保护访问权限 | protected | 派生子类可用 | 不希望所有人都可以使用, 但是希望此方法子类可以使用或更改 |
包访问权限 | (default) | 同一包内可以访问 | 仅希望同一个包内其他的类可以使用它 |
私有访问权限 | private | 仅自己类内部可以使用 | 完全私有方法, 包含类的对象都不可以调用, 一般用于实现类的私有能力, 绝不对外开放 |
5. 垃圾回收
前面用大量的篇幅讲解了如何创建一个实例以及如何使用这个实例。但是这些实例使用完 之后去了哪里呢?如果学过C++, 就知道如果C++创建完对象后置之不理,整个程序肯定会崩溃。但是这个问题对于 Java 来讲就没有那么重要了, Java 有一套自动回收的机制用于处理创建出来的实例.
继承与多态
继承是指派生类继承基类的属性和某些行为,多态是指派生类在基类的基础上进行重写从而表现出来的不同性状
1. Object
观察下面代码的输出, 了解类和对象的其他特性:
public class Person { private long id; private String name; public Person(long id, String name) { this.id = id; this.name = name; } public static void main(String[] args) { Person person = new Person(20, "Sunhr"); System.out.println(person.toString()); // 输出为:[email protected] }}
在这个例子中, 创建了一个person对象, 然后调用了toString()方法, 代码很简单, 问题是, 这个方法从哪来的呢?
在Java中, 所有的类都继承自Object类. 也就是说除了基本类型, 其他类都是一种Object, 而Object中就可以找到toString()方法, person也就是通过类的继承得到了这个方法.类似的还用equals方法等等
2. 组合
在了解继承之前, 先弄清除什么是组合. 手机相对于物质来讲, 属于继承各系, 它继承了物质这个大概念下的一些属性; 手机对于cpu来讲, 属于包含关系, 这种包含叫做组合.
对刚才的Person做出修改:
public class Person { private long id; private String name; private Eyes eyes = new Eyes(); public static class Eyes { public String left = "左眼"; public String right = "右眼"; }}
这里的Person包含了一个静态内部类的实例. Person是对一个事物的抽象, 但是这个事物是有很多部分组成的, 每个部分也可以抽象出来, 最后在Person中组合在一起.
3. 继承
继承是在同一种共性基础上的细分和丰富, 在基类中定义此类事物的共性, 在派生类中对基类中定义的共性进行具体的实现或者修改, 并且添加自己的特性.
public class Animal { public int weight; public Animal(int weight) { this.weight = weight; } public void move() { "Animal can move".sout }}public class Cat extends Animal { public String roar = "ao"; public Cat(int weight, String roar) { super(weight); this.roar = roar; } @Override public void move() { "Cat can move".sout }}
这个例子中先定义了基类Animal, 在基类中定义了动物的共有属性weight和方法move(); 派生类Cat中通过extends声明继承Animal, 添加了自己的属性roar, 并且重写了move方法.
引入了继承后, 一个对象的构造顺序又变得更加复杂了, 当创建一个派生类对象的时候, 原则是先构造此派生类的基类部分, 再构造派生类的新定义部分.
4. 多态
动态绑定: 执行时判断所作用对象的实际类型
多态的实现基于动态绑定, 是指用基类的引用指向派生类的实例, 当调用方法时再确定应该调用基类的方法还是派生类的方法.基于上面的例子, 再次引入一个Fish:
public class Fish extends Animal { public String livein = "water"; public Fish(int weight, String livein) { super(weight); this.livein = livein; } @Override public void move() { "Fish can swim!".sout }}
下面时分别创建Animal, Cat, Fish并引用这三个实例进行操作的例子:
public static void main(String[] args) { Animal animal = new Animal(10); Cat cat = new Cat(10, "ao!"); Fish fish = new Fish(10, "water"); Animal animals = new Animal[3]; animals[0] = animal; animals[1] = cat; animals[2] = fish; for(Animal tmp: animals) { tmp.move(); }}
这段代码的for循环中, 都是用Animal引用指代数组中实例, 但是调用move方法时却有不同的表现, 这就是多态. 多态是用基类指代派生类, 在实际调用时调用派生类的实现. 通过基类的引用可以引用基类中定义的字段, 例如weight, 但是无法使用派生类中添加的字段.
5. 接口
设想, 把上面的一堆Cat类的实例放到一个容器中(比如List), 希望按照每个实例的重量从小到大排序, 应该怎么办? 解决的方法就是接口, Cat可以通过实现Comparable中的compareTo()方法使得允许Cat类定义实例间的比较方式.
6. 抽象类
抽象类就是不可创建实例的基类. 比如之前的Animal可以做出如下修改:
public abstract class Animal { public int weight; public Animal(int weight) { this.weight = weight; } public abstract void move();}
容器
容器是存放对象的地方, 当大量的对象需要在内存中存在, 并且单个对象分别使用很不方便的时候, 就是容器的应用场景. Java存放数据的方式有很多种, 例如固定大小的数据以及可以自动调整大小的容器类. 而容器类经过多个版本的迭代, 继承关系较为复杂, 目前比较常用的有List, Set, Map
1. 数组
数组对于容器类, 效率更高, 但是在生命周期内不可改变数组大小, 数组有length字段, 用于访问数组的大小. “[]”的语法可以访问数组成员, 数组有多维的能力, 可以创建二维或以上的数组, 下面将演示一维数组与二维数组的创建
String[] arr1 = new String[5];String[][] arr2 = new String[2][];arr2[0] = new String[2];arr2[1] = new String[4];
2. List
容器List是一个列表, 但是Java对列表的实现有两种, 一种是类似数组的实现(ArrayList), 一种是类似链表的实现(LinkedList), 这两种List都可以通过List类进行引用并调用方法. ArrayList在插入方面不如LinkedList, 但是LinkedList在获取列表中的值方面不如ArrayList. 实际使用时可以根据情况选择. 下面将演示List的创建与增删查改等
List<String> list = new ArrayList<String>();// List<String> list = new LinkedList<String>();list.add("one");list.add("two");list.add("three");list.get(2);list.remove("three");list.contains("one");list.set(1, "2th");list.indexOf("2th");
3. Set
Set是一个集合, 它不保证存取的顺序, 它的主要特征是储存值的唯一性, 判断储存的对象是否相等, 可以使用equals和hashCode方法. Set同样分为两种: HashSet和TreeSet
4. Map
Map通过键值对储存, 可以通过键来获取值. HashMap是最常用的Map. 其通过散列的形式以达到快速存储和空间控制的目的.
泛型
泛型最常见的使用场景是在容器内, 容器提供了储存对象的通用能力, 其他所有类型的对象都可以放入容器之内.
1. 泛型的基本使用
class A { public void print() { "I'm A".sout } }class B extends A { public void print() { "I'm B".sout } }class C extends A { public void print() { "I'm C".sout } }public class TempleteTypeErase { public static <T> void print(List<T> list) { for(T t : list) { if( t instanceof A ) { "I'm A".sout } else if( t instanceof B ) { "I'm B".sout } else if( t instanceof C ) { "I'm C".sout } else { "I'm ?".sout } } } public static void main(String[] args) { List<A> as = new ArrayList<A>(); as.add(new A()); as.add(new B()); as.add(new C()); for(A a : as) { a.print(); } print(as); /* * I'm A * I'm B * I'm C * I'm A * I'm B * I'm C */ }}
2. 通配符
上面例子中, A是B的基类, 但是List<A>
与List<B>
之间没有任何关系! 通配符主要用于参数判断, 例如某个参数需要A的子类的容器, 通配符只允许获取数据, 即无法通过List<? extends A>
这种向容器添加数据. 原因是这个容器指代了一切继承自基类的容器, 无法确定是否正确的向容器中添加了适当类型.
public static void testExtend() { List<B> bs = new ArrayLisy<>(); bs.add(new B()); List<? extends A> as = bs; as.get(0).print();}public static void testSuper() { List<A> as = new ArrayLisy<>(); as.add(new A()); List<? super B> bs = as; bs.get(0).print();}
3. 泛型接口
Java的泛型区别于C++的泛型主要在类型擦除. 就是说Java泛型只在编译期间进行静态类型检查, 编译器生成的代码会擦除相应的类型信息, 这样JVM根本不知道泛型所代表的具体类型. 可以定义一个泛型接口, 然后让泛型类声明传进的具体类型必须实现此接口, 这样还保留部分接口能力. 常用的泛型接口时Comparable<T>
, 可以保留对象比较的能力.
/* 一个泛型接口 */public interface PrintInterface<T> { public void print(); /* 保留了打印能力 */}
4. 自定义泛型
class Tomato implements PrintInterface<Tomato> { @Override public void print(){ "It's Tomato!".sout }}public class CustomTemplete<T extends PrintInterface<T> > { public T data; public void print() { data.print(); } public static void main(String[] args) { CustomTemplete<Tomato> customTemplete = new CustomTemplete<>(); customTemplete.data = new Tomato(); customTemplete.print(); }}
异常
对于使用java编写的程序, 编译器在编译的时候会进行语法检查等工作. 但是有一些程序中存在的问题在编译阶段无法识别的, 例如用Java实现一个计算器, 当用户输入1/0的时候, 你应该怎么办? 这就是异常处理存在的原因. 这些错误会导致程序无法继续运行, 而异常处理就是处理这些错误的.
1. 运行时异常
运行时异常时程序在执行过程中出现错误调用而抛出的异常, 这种异常可以在编写时避免.
public class JavaRuntimeException { public static void testDivisor() { int i = 1/0; } public static void main(String[] args) { testDivisor(); }}
运行结果如下:
Exception in thread "main" java.lang.ArithmeticException: / by zero at JavaRuntimeException.testDivisor(JavaRuntimeException.java:3) at JavaRuntimeException.main(JavaRuntimeException.java:6)
用一个整数除以0在算术上是明显错误的, 但是这个问题编译器目前是不会报错的, 只会在执行时抛出一个异常, 异常包含错误的类型和代码的位置, 可以找到问题的所在并进行优化, 下面用异常捕获来处理这段代码:
public static void testDivisor() { try { int i = 1 / 0; System.out.println("i = " + i); } catch (Exception e) { System.out.println("divisor can not be zero"); }}/** divisor can not be zero*/
这里使用了try/catch进行了代码运行和异常捕获, 当然, 这里只作为演示, 实际项目中一般先用if进行判断, 而不用异常捕获来进行处理. 下面的代码演示了空引用异常:
public static void testNullPoint(){ Person person = null; System.out.println(person.id);}/*Exception in thread "main" java.lang.NullPointerException at JavaRuntimeException.testNullPoint(JavaRuntimeException.java:12) at JavaRuntimeException.main(JavaRuntimeException.java:15)*/
这种空异常的避免方法一般也是在调用前对不确定是否初始化的对象进行非空判断, 从而避免此异常.
下面为常见的List异常.
public static void testArrayRemove() { List<String> list = new ArrayList<>(); list.add("one"); list.add("two"); list.add("three"); int idx = 0; for (String string : list) { System.out.println(string); idx ++; if(idx == 1) { list.remove(idx); } }}/*oneException in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911) at java.util.ArrayList$Itr.next(ArrayList.java:861) at JavaRuntimeException.testArrayRemove(JavaRuntimeException.java:23) at JavaRuntimeException.main(JavaRuntimeException.java:32)*/
Java的运行时异常有很多种, 例如数组越界异常, 类型转换异常等等. 异常的处理不是背出来的, 在实际的代码种去解决异常才是学习方法.
2. 检查性异常
运行时异常基本都可以避免, 只要代码足够严谨就不会出现运行时异常. 所以真正代码中要处理的时检查性异常. 这就涉及异常的抛出和捕获, 在抛出异常的地方使用throw关键字抛出, 在抛出异常的方法后添加throws 异常类名
. 异常捕获的地方使用try-catch-finally
. 这里以读取文件作为例子:
public static String readFile() { boolean bool = true; StringBuilder builder = new StringBuilder(); try { FileReader fileReader = new FileReader("src/test.txt"); char[] cs = new char[10]; while (fileReader.read(cs) != -1) { builder.append(cs); cs = new char[10]; } } catch (Exception e) { bool = false; e.printStackTrace(); } finally { if(bool) { System.out.println("read file ok!"); } else { System.out.println("read file fail!"); builder.replace(0, builder.length(), "fail"); } } return builder.toString();}/*read file ok!12345678900987654321end */
在这个例子中, 使用文件读写类 FileReader读取一个文件, 在创建这个类的时候, 编译器强制要求把这个方法放入一段try语句中, 或者这个方法向外抛出异常(添加throws)由外层进行处理, 否则编译不通过; 这是区别于运行时异常的地方, 运行时异常运行编译通过.
3. 自定义异常
在实际编程中, 可能希望抛出一个已有的异常类型无法准确表达的异常, 例如数据库中的数据出现了业务逻辑上的错误. 这时候就需要自己定义一个异常, 自己定义异常只需要继承相关的异常类就可以.
public class CustomRuntimeException extends RuntimeException {}public class CustomException extends Exception{ }public class CustomExceptionDemo { public static void testRuntimeException() { throw new CustomRuntimeException(); } public static void testException() throws CustomException { throw new CustomException(); } public static void main(String[] args) { try { testException(); } catch(CustomException e) { "catch CustomException".sout } catch(Exception e) { "catch Exception".sout } }}/** catch CustomException*/
上面自定义了两个异常, 但是两个异常的继承关系不一样, 这两种继承关系会导致实际使用中存在区别. 继承自RuntimeException
的异常当用throw抛出时, 包含它的方法不需要用throws声明要抛出异常, 而继承自Exception则需要. 用catch捕获异常是按照顺序从第一个匹配的异常类型进行捕获的, 一般会把基类Exception放到最后, 防止它拦截了其他的捕获.
异常还包含其他的一些方法可供使用, 但是对于简单情况, 只需要继承一个异常, 并且用类的名字来区分异常类型就可以了.
I/O
实际的业务中存在大量的交互和通信需求, 这就需要对I/O有相应的了解. 有了I/O才能把程序组成一个庞大的系统, 否则只能是一个个程序计算的孤岛. 虽然很多组件都封装了简单易用的I/O操作, 但是了解一些Java的基本I/O还是对学习有帮助的.
1. 控制台I/O
在IDE中负责控制台输入输出的就是console窗口, 下面通过两种不同的I/O流分别读取此数据, 观察两种不同I/O的读取结果, 代码如下:
public static void testConsoleStreamIO() { try { char c; InputStream in = System.in; do { c = (char) in.read(); System.out.println(e); } while(c != 'q'); } catch (Exception e) { System.out.println("catch Exception"); }}public static void testConsoleBufferIO() { try { char c; BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); do { c = (char) in.read(); System.out.println(e); } while(c != 'q'); } catch (Exception e) { System.out.println("catch Exception"); }}
第一种方法直接获取系统的字节输入流, 读取流数据分别显示到控制台; 第二种方法用字符流封装了系统输入流, 然后读取数据显示至控制台. 使用这两种方法, 当读取全英文时没有分别. 但是读取汉字时, 使用字符流可以准确读取汉字, 字节流则不能. 所以读取二进制文件时(例如音频, 图片等), 使用字节流较为合适, 当读取汉字时, 使用字符流是合适的方式.
2. 查看文件列表
File类是Java对目录和文件进行操作的类, 可以用它对文件进行创建, 改名, 删除等操作. 下面以遍历目录为例, 简单介绍File类的使用.
import java.io.File;import java.util.ArrayList;import java.util.List;public class FileListDemo { public static List<String> getFileByDir(File dir) { List<String> list = new ArrayList<>(); for(File item: dir.listFiles()) { if(item.isDirectory()) { list.addAll(getFileByDir(item)); continue; } list.add(item.getName()); } return list; } public static List<String> getFileList(String uri) { List<String> list = new ArrayList<>(); File file = new File(uri); if(file.isDirectory()) { list.addAll(getFileByDir(file)); } else if (file.exists() && file.isFile()) { list.add(uri); } else if (!file.exists()) { System.out.println("file not found"); } return list; } public static void main(String[] args) { System.out.println(getFileList("../")); }}
3. 文件I/O
文件的读写在检查性异常已经涉及, 此处不再做演示
4. 序列化
当把一个Java对象存入文件或者进行网络通信时, 需要把一个对象转为一串数据, 并可以再反转成一个对象, 这就是序列化的需求. Java的序列化用几种方式: 对象的类继承Serializable接口, 或者对象的类继承Externalizable接口, 实现两个接口的方法; 或者转换为其他通用格式, 例如Json
-
Serializable实现序列化
import java.io.*;public class JavaSerialize { static public class Address implements Serializable { public double longitude; public double latiude; public String name; public transient Person person; } public static void write(String uri, Address address) { try { File file = new File(uri); FileOutputStream outputStream = new FileOutputStream(file); ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); objectOutputStream.writeObject(address); objectOutputStream.close(); outputStream.close(); } catch (Exception e) { e.printStackTrace(); } } public static Address Read(String uri){ Address address = null; try { File file = new File(uri); FileInputStream inputStream = new FileInputStream(file); ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); address = (Address) objectInputStream.readObject(); objectInputStream.close(); inputStream.close(); } catch (Exception e) { e.printStackTrace(); } return address; } public static void main(String[] args) { ... }}
此例中, Address继承了Serializable接口, 并且对person标志了transient表示无需序列化.
-
Externalizable实现序列化
此接口包含两个方法,readExternal
和writeExternal
, 可以通过这两个方法完成序列化的定制....static public class Address implements Externalizable { public double longitude; public double latitude; public String name; public transient Person person; @Override public void readExternal(ObjectInput arg0) throws IOException, ClassNotFoundException { longitude = arg0.readDouble(); latitude = arg0.readDouble(); name = (String) arg0.readObject(); } @Override public void writeExternal(ObjectOutput arg0) throws IOException { arg0.writeDouble(longitude); arg0.writeDouble(latitude); arg0.writeObject(name); }}...
- Json
Json时一种轻量级的数据交换格式, 可以把对象序列化为Json格式
5. 网络I/O(待补)
Java服务之间可以通过网络进行通信, 从而实现程序间数据的互通, 网络I/O是Java服务进行微服务化的基础. 网络通信一般较为复杂, 一般都由使用的服务框架解决. 所以我tm也不会, 待补吧
并发
一个Java程序运行在一个进程中, 但是, 有的时候你希望一个程序可以同时做好多事情, 比如监听端口的同时接受数据并进行逻辑计算, 只有一个运算单元明显不够了, 所以这时需要启动好多个运算单元, 这就是所谓的多线程. 由于多线程本身是一个比较难的知识点, 在此主要是介绍多线程的写法和一些重点
1. 多线程实现
-
Runnable任务
public class ThreadRunnable implements Runnable { private int start; private int end; public ThreadRunnable(int start, int end) { this.start = start; this.end = end; } @Override public void run() { int sum = 0; for (int i = start; i <= end; ++ i) { sum += i; } "thread is "+Thread.currentThread().getName()+" start = "+start+"end ="+end+"sum = "+sum.sout } public static void main(String[] args) { ThreadRunnable runnable = new ThreadRunnable(100, 1000); runnable.run(); Thread thread = new Thread(new ThreadRunnable(200, 2000)); thread.start(); /* * thread is main start = 100 end = 1000 sum = 495550 * thread is Thread-0 start = 200 end = 2000 sum = 1981100 */ }}
ThreadRunnable 个继承自 Runnable 的类,如果在主线程中创建这个类,并且调用run 方法,其实它并没有什么特殊,只是正常执行求和的逻辑. Runnable 对多线程的作用就是可以把它传入 Thread 中,作为新建线程的执行任务,这样就实现了多线程.
-
自定义Thread
public class CustorThread extends Thread { @Override public void run() { try { Thread.sleep(1000); } catch(Exception e) { e.printStackTrace(); } this.sout } public static void main(String[] args) { CustorThread thread1 = new CustorThread(); thread1.setPriority(Thread.MAX_PRIORITY); CustorThread thread2 = new CustorThread(); thread2.setPriority(Thread.MAX_PRIORITY); thread1.start(); thread2.start(); /* * Thread[Thread-1,5,main] * Thread[Thread-0,1,main] */ }}
可以让一个类继承自Thread类, 重写run方法来实现线程的任务单元, 这样就可以不用把任务单元传给Thread, 只要创建此线程并调用start即可实现多线程
-
线程池
以上两种都需要手动创建线程, 可以使用线程池进行托管, 省去麻烦的同时还可以复用ExecutorService eService = Executors.newCachedThreadPool();// ExecutorService eService = Executors.newFixedThreadPool();for(int i = 0; i < 10; ++ i) { eService.execute(new ThreadRunnable(i*100, i*1000));}eService.shutdown();
2. 线程冲突
上面那几个例子创建的都是独立的任务单元, 但是如果多个线程中的任务单元是相同的, 且使用了同一份数据, 那么会发生一些问题.
public class ThreadConflict { private int sum; public int getSum(int start, int end) { sum = 0; for(int i = start; i < end; ++ i) { sum += i; } return sum; } public static void main(String[] args) { ThreadConflict threadConflict = new ThreadConflict(); "main thread sum = " + threadConflict.getSum(0, 1000).sout; ExecutorService eService = Executors.newCachedThreadPool(); for(int i = 0; i < 10; ++ i) { eService.execute(new Runnable() { @Override public void run() { Thread.currentThread().getName() + " sum = " + threadConflict.getSum(0, 1000).sout } }) } eService.shutdown(); }}/*main thread sum = 499500pool-1-thread-1 sum = 499500pool-1-thread-2 sum = 499500pool-1-thread-3 sum = 499500pool-1-thread-1 sum = 499500pool-1-thread-4 sum = 499500pool-1-thread-2 sum = 499500pool-1-thread-6 sum = 499500pool-1-thread-7 sum = 499500pool-1-thread-5 sum = 327769pool-1-thread-8 sum = 743833*/
Java的多线程是抢占式的, 当多个线程抢占同一资源时, 可能线程A运算到一半时B抢占了A重新开始计算, 等到A回去时资源数据已经不是它离开时的数据了. 这种情况下可以使用锁解决并发导致的资源抢占问题
3. 锁
- Synchronized关键字
只需要在getSum
前加上关键字变为public synchronized int getSum
, 就不会出现上面的情况了. 该关键字把这个方法设置为同步方法, 当有多个线程希望使用此方法时, 此关键字只允许一个线程独享此方法, 其他线程处于等待状态 -
Lock
可以在这个对象中创建一个ReentrantLock实例, 对需要加锁的代码段前面调用lock方法上锁, 执行完毕调用unlock解锁. (此实例包含其他几种加锁方式, 例如tryLock方法)
private Lock lock = new ReentrantLock();public int getSumByLock(int start, int end) { lock.lock(); try { ... } finally { lock.unlock(); }}public int getSumByTryLock(int start, int end) { try { if(lock.trylock(1, TimeUnit.SECONDS)){ ... } } catch(InterruptedException e){ e.printStackTrace(); return -1; } finally { lock.unlock(); }}
- 读写锁: ReentrantReadWriteLock
反射与注解
对于一个语言来讲, 前面的内容仿佛已经足够全面了, 那么Java的反射和注解为Java做出了什么贡献?
Java的反射机制是指在运行状态中, 对于任意一个类, 都可以知道这个类的所有属性和方法; 对于任意一个对象, 都能够调用他的任意方法和属性. 这种动态获取信息以及动态调用对象的功能称为Java的反射机制.
注解可以理解为Java对类, 字段或方法的补充说明, Java通过反射读到注解, 通过注解的说明对被注解的内容进行相应的操作.
Java的反射与注解的意义在于与框架结合, 各个框架正是应用了Java 的反射与注解才能对业务代码进行加载和整合.
1. 反射
如果没有特殊需求, 一般的业务逻辑中不会带有反射, 了解反射的用处和能力即可, 如果有更高的需求, 那么反射还是要仔细研究的.
2. 注解
通过注解可以更加了解代码, 并可通过注解解析和使用方便管理代码
- Java内置三种注解:
@Override: 当前方法覆盖基类方法
@Deprecated: 表示弃用的方法
@SuppressWarnings: 关闭警告的注解
- 元注解
@Target: 表示注解的可用范围, 包含CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE. PARAMETER, TYPE
@Retention: 表示注解的应用级别, 分为:SOURCE. CLASS, RUNTIME
@Documented: 可以被Javadoc文档化
@Inherited: 注解类型被自动继承
-
自定义注解
自定义注解和接口非常相似, 比如:
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic @interface MethodUrl { public int ID() default -1; public String Describe(); public String URL();}/** 使用: @MethodUrl(ID=1, Describe="获取名字",URL="getName")*/
-
注解解析
比如上面的自定义注解, 这个注解写完了怎么用?难度仅仅在文档上有用吗?下面给一个类加上自定义注解, 然后再通过一个解析的方法把注解解析出来
public class JavaAnnotation { public String name; @MethodUrl(ID=1, Describe="获取名字",URL="getName") public String getName() { return name; }}
上面代码建立了一个类, 下面的代码可以获取到JavaAnnotation类的注解情况并输出
public static void main(String[] args) { try { Class clazz = Class.forName("xxxxxxx.JavaAnnotation"); Method[] methods = clazz.getMethods(); for ( Method method : methods) { MethodUrl methodUrl = method.getDeclaredAnnotation(MethodUrl.class); if(methodUrl != null) { method.getName() + " function ID is " + methodUrl.ID() + " and url is localhost" + methodUrl.URL() + " for " + methodUrl.Describe().sout } } } catch (Exception e) { e.printStackTrace(); }}
JUnit
当编写完代码, 需要对自己写的功能进行测试时, 可以直接写一个main来测试自己的代码, 也可以使用JUnit进行单元测试. JUnit可以保证程序稳定性的同时减少花费在排错上的时间
1. JUnit的集成
如果非Maven管理的项目有如下两种选择:
- 可以在官网下载最新版压缩文件, 解压后将需要的jar包放到libs文件夹内
- 选择Build -> Add Library, 选中JUnit, 点击Finish
对于Maven管理的项目, 可以使用如下代码:
<!-- JUnit test --><dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope></dependency>
2. JUnit的基本使用
- 创建一个测试类
- 在测试类中添加一个方法
- [email protected]
- 执行JUnit的assertEquals来检查测试是否通过
3. JUnit常用注解
注解名称 | 含义 |
---|---|
@Before | 运行前调用, 一般用于初始化方法 |
@After | 运行后调用, 一般用于释放资源 |
@Test | 测试方法, 可以测试方法的执行情况 |
@BeforeClass | 所有用例运行之前只执行一次, 且方法必须为static void |
@AfterClass | 所有用例运行之后只执行一次, 且方法必须为static void |
@Ignore | 忽略的测试方法 |
你还在原价购买阿里云、腾讯云、华为云、天翼云产品?那就亏大啦!现在申请成为四大品牌云厂商VIP用户,可以3折优惠价购买云服务器等云产品,并且可享四大云服务商产品终身VIP优惠价,还等什么?赶紧点击下面对应链接免费申请VIP客户吧:
相关文章
- Java 多线程(七):线程池
- Java 多线程(五):锁(三)
- Java 多线程(四):锁(二)
- Java 多线程(三):锁(一)
- Java 多线程(二):并发编程的三大特性
- Java 多线程(一):基础
- Java SE 18 新增特性
- Java SE 17 新增特性
- Java SE 16 新增特性
- Java SE 15 新增特性
- Java SE 14 新增特性
- Java SE 10 Application Class-Data Sharing 示例
- Java SE 13 新增特性
- Java SE 12 新增特性
- Java SE 11 新增特性
- Java SE 10 新增特性
- Java SE 9 模块化示例
- Java SE 9 多版本兼容 JAR 包示例
- Java SE 9 新增特性
- Java SE 8 新增特性