数组的定义:
数组是相同类型数据的有序集合。数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。其中,每一个数据称作一个元素,每个元素可以通过一个索引(下标)来访问它们。数组的三个基本特点:
1. 长度是确定的。数组一旦被创建,它的大小就是不可以改变的。
2. 其元素必须是相同类型,不允许出现混合类型。
3. 数组类型可以是任何数据类型,包括基本类型和引用类型。
注意:
数组变量属引用类型,数组也可以看成是对象,数组中的每个元素相当于该对象的成员变量。数组本身就是对象,Java中对象是在堆中的,因此数组无论保存原始类型还是其他对象类型,数组对象本身是在堆中存储的。
public class test1 {
public static void main(String[] args) {
String[] array1;//声明一个字符串数组
int[] array2;//声明一个整型数组
int[] array3=new int[10];//声明数组并指定数组长度
array3[0]=1;//为array3第一个元素赋值
//数组下标的范围为[0,lenth-1],超过数组索引下标无法赋值
System.out.println(array3[0]);//1
System.out.println(array3.length);//10,打印数组的长度
}
}
注意事项
1. 声明的时候并没有实例化任何对象,只有在实例化数组对象时,JVM才分配空间,这时才与长度有关。
2. 声明一个数组的时候并没有数组真正被创建。
3. 构造一个数组,必须指定长度。
为数组赋值:
public class test2 {
public static void main(String[] args) {
int[] array=new int[10];
for(int i=0;i<array.length;i++){
array[i]=i+1;
System.out.print(array[i]);//输出1到10
}
}
}
public class test3 {
public static void main(String[] args) {
User[] users=new User[2];
users[0]=new User("test1",18);
users[1]=new User("test2",19);
System.out.println(users[0].getName());//输出test1
}
}
class User{
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
}
数组的初始化方式总共有三种:静态初始化、动态初始化、默认初始化。
静态初始化
除了用new关键字来产生数组以外,还可以直接在定义数组的同时就为数组元素分配空间并赋值。
int[] a = { 1, 2, 3 };// 静态初始化基本类型数组;
Man[] mans = { new Man(1, 1), new Man(2, 2) };// 静态初始化引用类型数组;
动态初始化
数组定义与为数组元素分配空间并赋值的操作分开进行。
int[] a1 = new int[2];//动态初始化数组,先分配空间;
a1[0]=1;//给数组元素赋值;
a1[1]=2;//给数组元素赋值;
数组的默认初始化
数组是引用类型,它的元素相当于类的实例变量,因此数组一经分配空间,其中的每个元素也被按照实例变量同样的方式被隐式初始化。
int a2[] = new int[2]; // 默认值:0,0
boolean[] b = new boolean[2]; // 默认值:false,false
String[] s = new String[2]; // 默认值:null, null
增强for循环for-each是JDK1.5新增加的功能,专门用于读取数组或集合中所有的元素,即对数组进行遍历。
public class Test {
public static void main(String[] args) {
String[] s = { "aa", "bbb", "ccc", "ddd" };
for (String temp : s) {
System.out.println(temp);
}
}
}
注意事项:
1. for-each增强for循环在遍历数组过程中不能修改数组中某元素的值。
2. for-each仅适用于遍历,不涉及有关索引(下标)的操作。
System类里也包含了一个static void arraycopy(object src,int srcpos,object dest, int destpos,int length)方法,该方法可以将src数组里的元素值赋给dest数组的元素,其中srcpos指定从src数组的第几个元素开始赋值,length参数指定将src数组的多少个元素赋给dest数组的元素。
public class Test {
public static void main(String args[]) {
String[] src = {"阿里","京东","搜狐","网易"};
String[] dest = new String[6];
System.arraycopy(src,0,dest,0,src.length);
for (int i = 0; i < dest.length; i++) {
System.out.print(dest[i]+ "\t");
}
}
}
从数组中删除某个元素:
本质上还是数组的拷贝,自己拷贝自己。
public class test7 {
public static void main(String[] args) {
String[] s1={"aaa","bbb","ccc","ddd","eee"};
//删除ccc
System.arraycopy(s1,3,s1,2,s1.length-3);
for(int i=0;i<s1.length-1;i++){
System.out.println(s1[i]);
}
}
}
数组扩容:
本质:先定义一个更大的数组,再将原数组拷贝进去
public class test8 {
public static void main(String[] args) {
String[] s1={"a","b","c"};
String[] s2=new String[s1.length+5];//定义一个更大的数组
System.arraycopy(s1,0,s2,0,s1.length);//将s1中所有的元素拷贝到s2
for(String a:s2){
System.out.println(a);
}
}
}
JDK提供的java.util.Arrays类,包含了常用的数组操作,方便我们日常开发。Arrays类包含了:排序、查找、填充、打印内容等常见的操作。
打印数组:
public class test9 {
public static void main(String[] args) {
int[] a={1,2,3,4};
System.out.println(a);//打印数组引用的值
System.out.println(Arrays.toString(a));//打印数组元素的值;
}
}
此处的Arrays.toString()方法是Arrays类的静态方法,不是前面讲的Object的toString()方法。
输出结果:
[I@4554617c [1, 2, 3, 4]
数组元素的排序:
public class test10 {
public static void main(String[] args) {
int[] a={20,33,10,3,100,5,21};
System.out.println(Arrays.toString(a));
Arrays.sort(a);
System.out.println(Arrays.toString(a));
}
}
输出结果:
[20, 33, 10, 3, 100, 5, 21] [3, 5, 10, 20, 21, 33, 100]
数组元素是引用类型的排序:
自定义类型必须实现Comparable接口,并重写compareTo方法,在compareTo方法定义排序规则
public class test11 {
public static void main(String[] args) {
Man[] msMans = { new Man(3, "a"), new Man(60, "b"), new Man(2, "c") };
Arrays.sort(msMans);
//根据年龄从小到大排序输出姓名
System.out.println(Arrays.toString(msMans));
}
}
class Man implements Comparable {
int age;
int id;
String name;
public Man(int age, String name) {
super();
this.age = age;
this.name = name;
}
public String toString() {
return this.name;
}
public int compareTo(Object o) {
Man man = (Man) o;
if (this.age < man.age) {
return -1;
}
if (this.age > man.age) {
return 1;
}
return 0;
}
}
输出结果:
[c, a, b]
二分法查找:
查找数组中是否有指定元素
public class test12 {
public static void main(String[] args) {
int[] a = {1,2,323,23,543,12,59};
System.out.println(Arrays.toString(a));
Arrays.sort(a); //使用二分法查找,必须先对数组进行排序;
System.out.println(Arrays.toString(a));
//返回排序后新的索引位置,若未找到返回负数。
System.out.println("该元素的索引:"+Arrays.binarySearch(a, 12));
}
}
运行结果:
[1, 2, 323, 23, 543, 12, 59] [1, 2, 12, 23, 59, 323, 543] 该元素的索引:2
数组填充:
public class test13 {
public static void main(String[] args) {
int[] a= {1,2,323,23,543,12,59};
System.out.println(Arrays.toString(a));
Arrays.fill(a, 2, 4, 100); //将2到4-1索引的元素替换为100;
System.out.println(Arrays.toString(a));
}
}
多维数组可以看成以数组为元素的数组。可以有二维、三维、甚至更多维数组,但是实际开发中用的非常少。最多到二维数组(学习容器后,一般使用容器,二维数组用的都很少)。
二维数组的声明:
public class test14 {
public static void main(String[] args) {
// Java中多维数组的声明和初始化应按从低维到高维的顺序进行
//int[][]定义一个二维数组,二维数组内的每个元素为int[]
int[][] a = new int[3][];
a[0] = new int[2];
a[1] = new int[4];
a[2] = new int[3];
// int a1[][]=new int[][4];//非法
}
}
二维数组的静态初始化:
public class Test {
public static void main(String[] args) {
int[][] a = {
{ 1, 2, 3 },
{ 3, 4 },
{ 3, 5, 6, 7 }
};
System.out.println(a[2][3]);
}
}
内存分配图:
二维数组的动态初始化:
public class test15 {
public static void main(String[] args) {
int[][] a = new int[3][];
// a[0] = {1,2,5}; //错误,没有声明类型就初始化
a[0] = new int[] { 1, 2 };
a[1] = new int[] { 2, 2 };
a[2] = new int[] { 2, 2, 3, 4 };
System.out.println(a[2][3]);
System.out.println(Arrays.toString(a[0]));
System.out.println(Arrays.toString(a[1]));
System.out.println(Arrays.toString(a[2]));
System.out.println(a.length);//获取的二维数组有几个一维数组
System.out.println(a[0].length);//获取第二维第一个数组长度。
}
}
运行结果:
4 [1, 2] [2, 2] [2, 2, 3, 4] 3 2
算法重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来,这样越大的元素会经由交换慢慢“浮”到数列的顶端。
public class test16 {
public static void main(String[] args) {
int[] a={10,5,16,22,1,88,256,14};
int temp=0;
for(int i=0;i<a.length;i++){
for(int j=i+1;j<a.length;j++){
if(a[i]>a[j]){//比较大小,换顺序
temp=a[j];
a[j]=a[i];
a[i]=temp;
}
}
}
System.out.println(Arrays.toString(a));
}
}
输出结果:
[1, 5, 10, 14, 16, 22, 88, 256]
冒泡排序优化:
判断每一趟是否发生了数组元素的交换,如果没有发生,则说明此时数组已经有序,无需再进行后续趟数的比较了。此时可以中止比较。
import java.util.Arrays;
public class Test1 {
public static void main(String[] args) {
int[] values = { 3, 1, 6, 2, 9, 0, 7, 4, 5, 8 };
bubbleSort(values);
System.out.println(Arrays.toString(values));
}
public static void bubbleSort(int[] values) {
int temp;
int i;
// 外层循环:n个元素排序,则至多需要n-1趟循环
for (i = 0; i < values.length - 1; i++) {
// 定义一个布尔类型的变量,标记数组是否已达到有序状态
boolean flag = true;
/*内层循环:每一趟循环都从数列的前两个元素开始进行比较,比较到无序数组的最后*/
for (int j = 0; j < values.length - 1 - i; j++) {
// 如果前一个元素大于后一个元素,则交换两元素的值;
if (values[j] > values[j + 1]) {
temp = values[j];
values[j] = values[j + 1];
values[j + 1] = temp;
//本趟发生了交换,表明该数组在本趟处于无序状态,需要继续比较;
flag = false;
}
}
//根据标记量的值判断数组是否有序,如果有序,则退出;无序,则继续循环。
if (flag) {
break;
}
}
}
}
二分法检索(binary search)又称折半检索,二分法检索的基本思想是设数组中的元素从小到大有序地存放在数组(array)中,首先将给定值key与数组中间位置上元素的关键码(key)比较,如果相等,则检索成功;否则,若key小,则在数组前半部分中继续进行二分法检索;若key大,则在数组后半部分中继续进行二分法检索。
这样,经过一次比较就缩小一半的检索区间,如此进行下去,直到检索成功或检索失败。
二分法检索是一种效率较高的检索方法。比如,我们要在数组[7, 8, 9, 10, 12, 20, 30, 40, 50, 80, 100]中查询到是否有10元素,过程如下:
先将原数组按顺序排序,再取中间值mid,如果查找的数字比中间值小则继续在中间值的左边查找;如果查找的值比中间值大则在右边查找,直到查找完;
二分法查找实现:
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = { 30,20,50,10,80,9,7,12,100,40,8};
int searchWord = 20; // 所要查找的数
Arrays.sort(arr); //二分法查找之前,一定要对数组元素排序
System.out.println(Arrays.toString(arr));
System.out.println(searchWord+"元素的索引:"+binarySearch(arr,searchWord));
}
public static int binarySearch(int[] array, int value){
int low = 0;
int high = array.length - 1;
while(low <= high){
int middle = (low + high) / 2;
if(value == array[middle]){
return middle; //返回查询到的索引位置
}
if(value > array[middle]){
low = middle + 1;
}
if(value < array[middle]){
high = middle - 1;
}
}
return -1; //上面循环完毕,说明未找到,返回-1
}
}
异常机制本质:就是当程序出现错误,程序安全退出的机制。
异常指程序运行过程中出现的非正常现象,例如用户输入错误、除数为零、需要处理的文件不存在、数组下标越界等。
在Java的异常处理机制中,引进了很多用来描述和处理异常的类,称为异常类。异常类定义中包含了该类异常的信息和对异常进行处理的方法。所谓异常处理,就是指程序在出现问题时依然可以正确的执行完。
Java是采用面向对象的方式来处理异常的。处理过程:
1. **抛出异常:**在执行一个方法时,如果发生异常,则这个方法生成代表该异常的一个对象,停止当前执行路径,并把异常对象提交给JRE。
2. **捕获异常:**JRE得到该异常后,寻找相应的代码来处理该异常。JRE在方法的调用栈中查找,从生成异常的方法开始回溯,直到找到相应的异常处理代码为止。
JDK 中定义了很多异常类,这些类对应了各种各样可能出现的异常事件,所有异常对象都是派生于Throwable类的一个实例。如果内置的异常类不能够满足需要,还可以创建自己的异常类。
Java对异常进行了分类,不同类型的异常分别用不同的Java类表示,所有异常的根类为java.lang.Throwable,Throwable下面又派生了两个子类:Error和Exception。Java异常类的层次结构如图所示:
Error是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
Error表明系统JVM已经处于不可恢复的崩溃状态中。程序内部错误我们不需要管它。
Error与Exception的区别
1. 我开着车走在路上,一头猪冲在路中间,我刹车。这叫一个异常。
2. 我开着车在路上,发动机坏了,我停车,这叫错误。系统处于不可恢复的崩溃状态。发动机什么时候坏?我们普通司机能管吗?不能。发动机什么时候坏是汽车厂发动机制造商的事。
Exception是程序本身能够处理的异常,如:空指针异常(NullPointerException)、数组下标越界异常(ArrayIndexOutOfBoundsException)、类型转换异常(ClassCastException)、算术异常(ArithmeticException)等。
Exception类是所有异常类的父类,其子类对应了各种各样可能出现的异常事件。 通常Java的异常可分为:
RuntimeException 运行时异常
CheckedException 已检查异常
派生于RuntimeException的异常,如被 0 除、数组下标越界、空指针等,其产生比较频繁,处理麻烦,如果显式的声明或捕获将会对程序可读性和运行效率影响很大。 因此由系统自动检测并将它们交给缺省的异常处理程序(用户可不必对其处理)。
这类异常通常是由编程错误导致的,所以在编写程序时,并不要求必须使用异常处理机制来处理这类异常,经常需要通过增加“逻辑处理来避免这些异常”。
ArithmeticException异常:试图除以0
public class Test3 {
public static void main(String[] args) {
int b=0;
System.out.println(1/b);
}
}
运行后抛出异常:
解决如上异常需要修改代码:
public class Test3 {
public static void main(String[] args) {
int b=0;
if(b!=0){
System.out.println(1/b);
}
}
}
NullPointerException异常:
当程序访问一个空对象的成员变量或方法,或者访问一个空数组的成员时会发生空指针异常。
public class Test4 {
public static void main(String[] args) {
String str=null;
System.out.println(str.charAt(0));
}
}
运行后抛出异常:
解决空指针异常,通常是增加非空判断:
public class Test4 {
public static void main(String[] args) {
String str=null;
if(str!=null){
System.out.println(str.charAt(0));
}
}
}
ClassCastException异常:
在引用数据类型转换时,有可能发生类型转换异常(ClassCastException)。
class Animal{
}
class Dog extends Animal{
}
class Cat extends Animal{
}
public class Test5 {
public static void main(String[] args) {
Animal a=new Dog();
Cat c=(Cat)a;
}
}
运行后抛出异常:
解决ClassCastException的典型方式:
public class Test5 {
public static void main(String[] args) {
Animal a = new Dog();
if (a instanceof Cat) {
Cat c = (Cat) a;
}
}
}
ArrayIndexOutOfBoundsException异常:
当程序访问一个数组的某个元素时,如果这个元素的索引超出了0~数组长度-1这个范围,则会出现数组下标越界异常(ArrayIndexOutOfBoundsException)。
public class Test6 {
public static void main(String[] args) {
int[] arr = new int[5];
System.out.println(arr[5]);
}
}
运行后抛出异常:
解决数组索引越界异常的方式,增加关于边界的判断:
public class Test6 {
public static void main(String[] args) {
int[] arr = new int[5];
int a = 5;
if (a < arr.length) {
System.out.println(arr[a]);
}
}
}
NumberFormatException异常:
在使用包装类将字符串转换成基本数据类型时,如果字符串的格式不正确,则会出现数字格式异常。
public class Test7 {
public static void main(String[] args) {
String str = "1234abcf";
System.out.println(Integer.parseInt(str));
}
}
运行后抛出异常:
数字格式化异常的解决,可以引入正则表达式判断是否为数字:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Test7 {
public static void main(String[] args) {
String str = "1234abcf";
Pattern p = Pattern.compile("^\\d+$");
Matcher m = p.matcher(str);
if (m.matches()) { // 如果str匹配代表数字的正则表达式,才会转换
System.out.println(Integer.parseInt(str));
}
}
}
注意事项:
2. 运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。
所有不是RuntimeException的异常,统称为Checked Exception,又被称为“已检查异常”,如IOException、SQLException等以及用户自定义的Exception异常。 这类异常在编译时就必须做出处理,否则无法通过编译。
如图所示:
异常的处理方式有两种:使用“try/catch”捕获异常、使用“throws”声明异常。
捕获异常是通过3个关键词来实现的:try-catch-finally。用try来执行一段程序,如果出现异常,系统抛出一个异常,可以通过它的类型来捕捉(catch)并处理它,最后一步是通过finally语句为异常处理提供一个统一的出口,finally所指定的代码都要被执行(catch语句可有多条;finally语句最多只能有一条,根据自己的需要可有可无)。
1. try:
try语句指定了一段代码,该段代码就是异常捕获并处理的范围。在执行过程中,当任意一条语句产生异常时,就会跳过该条语句中后面的代码。代码中可能会产生并抛出一种或几种类型的异常对象,它后面的catch语句要分别对这些异常做相应的处理。
一个try语句必须带有至少一个catch语句块或一个finally语句块 。
注意事项
当异常处理的代码执行结束以后,不会回到try语句去执行尚未执行的代码。
2. catch:
每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常对象。
常用方法,这些方法均继承自Throwable类 。
toString ()方法,显示异常的类名和产生异常的原因
getMessage()方法,只显示产生异常的原因,但不显示类名。
printStackTrace()方法,用来跟踪异常事件发生时堆栈的内容。
catch捕获异常时的捕获顺序:
如果异常类之间有继承关系,在顺序安排上需注意。越是顶层的类,越放在下面,再不然就直接把多余的catch省略掉。 也就是先捕获子类异常再捕获父类异常。
2. finally:
有些语句,不管是否发生了异常,都必须要执行,那么就可以把这样的语句放到finally语句块中。
通常在finally中关闭程序块已打开的资源,比如:关闭文件流、释放数据库连接等。
try-catch-finally语句块的执行过程:
try-catch-finally程序块的执行流程以及执行结果比较复杂。
基本执行过程如下:
程序首先执行可能发生异常的try语句块。如果try语句没有出现异常则执行完后跳至finally语句块执行;如果try语句出现异常,则中断执行并根据发生的异常类型跳至相应的catch语句块执行处理。catch语句块可以有多个,分别捕获不同类型的异常。catch语句块执行完后程序会继续执行finally语句块。finally语句是可选的,如果有的话,则不管是否发生异常,finally语句都会被执行。
注意事项
即使try和catch块中存在return语句,finally语句也会执行。是在执行完finally语句后再通过return退出。
finally语句块只有一种情况是不会执行的,那就是在执行finally之前遇到了System.exit(0)结束程序运行。
典型捕获异常代码:
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Test8 {
public static void main(String[] args) {
FileReader reader = null;
try {
reader = new FileReader("d:/a.txt");
char c = (char) reader.read();
char c2 = (char) reader.read();
System.out.println("" + c + c2);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
当CheckedException产生时,不一定立刻处理它,可以再把异常throws出去。
在方法中使用try-catch-finally是由这个方法来处理异常。但是在一些情况下,当前方法并不需要处理发生的异常,而是向上传递给调用它的方法处理。
如果一个方法中可能产生某种异常,但是并不能确定如何处理这种异常,则应根据异常规范在方法的首部声明该方法可能抛出的异常。
如果一个方法抛出多个已检查异常,就必须在方法的首部列出所有的异常,之间以逗号隔开。
典型声明异常代码:
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Test9 {
public static void main(String[] args) {
try {
readFile("joke.txt");
} catch (FileNotFoundException e) {
System.out.println("所需文件不存在!");
} catch (IOException e) {
System.out.println("文件读写错误!");
}
}
public static void readFile(String fileName) throws FileNotFoundException,
IOException {
FileReader in = new FileReader(fileName);
int tem = 0;
try {
tem = in.read();
while (tem != -1) {
System.out.print((char) tem);
tem = in.read();
}
} finally {
in.close();
}
}
}
注意:方法重写中声明异常原则:子类重写父类方法时,如果父类方法有声明异常,那么子类声明的异常范围不能超过父类声明的范围。
1.在程序中,可能会遇到JDK提供的任何标准异常类都无法充分描述清楚我们想要表达的问题,这种情况下可以创建自己的异常类,即自定义异常类。
2.自定义异常类只需从Exception类或者它的子类派生一个子类即可。
3.自定义异常类如果继承Exception类,则为受检查异常,必须对其进行处理;如果不想处理,可以让自定义异常类继承运行时异常RuntimeException类。
4.习惯上,自定义异常类应该包含2个构造器:一个是默认的构造器,另一个是带有详细信息的构造器。
自定义异常类:
/**IllegalAgeException:非法年龄异常,继承Exception类*/
class IllegalAgeException extends Exception {
//默认构造器
public IllegalAgeException() {
}
//带有详细信息的构造器,信息存储在message中
public IllegalAgeException(String message) {
super(message);
}
}
自定义异常类的使用:
class Person {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) throws IllegalAgeException {
if (age < 0) {
throw new IllegalAgeException("人的年龄不应该为负数");
}
this.age = age;
}
public String toString() {
return "name is " + name + " and age is " + age;
}
}
public class TestMyException {
public static void main(String[] args) {
Person p = new Person();
try {
p.setName("test");
p.setAge(-1);
} catch (IllegalAgeException e) {
e.printStackTrace();
System.exit(-1);
}
System.out.println(p);
}
}
执行结果抛出自定义异常:
使用异常机制的建议
1.要避免使用异常处理代替错误处理,这样会降低程序的清晰性,并且效率低下。
2.处理异常不可以代替简单测试---只在异常情况下使用异常机制。
3.不要进行小粒度的异常处理---应该将整个任务包装在一个try语句块中。
4.异常往往在高层处理 。
Java是面向对象的语言,但并不是“纯面向对象”的,因为我们经常用到的基本数据类型就不是对象。但是我们在实际应用中经常需要将基本数据转化成对象,以便于操作。比如:将基本数据类型存储到Object[]数组或集合中的操作等等。
为了解决这个不足,Java在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。
包装类均位于java.lang包,八种包装类和基本数据类型的对应关系如下表所示:
在这八个类名中,除了Integer和Character类以外,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写而已。
在这八个类中,除了Character和Boolean以外,其他的都是“数字型”,“数字型”都是java.lang.Number的子类。Number类是抽象类,因此它的抽象方法,所有子类都需要提供实现。Number类提供了抽象方法:intValue()、longValue()、floatValue()、doubleValue(),意味着所有的“数字型”包装类都可以互相转型。
初识包装类:
public class WrapperClassTest {
public static void main(String[] args) {
Integer i = new Integer(10);
Integer j = new Integer(50);
}
}
内存分析如图:
对于包装类来说,这些类的用途主要包含两种:
1. 作为和基本数据类型对应的类型存在,方便涉及到对象的操作,如Object[]、集合等的操作。
2. 包含每种基本数据类型的相关属性如最大值、最小值等,以及相关的操作方法(这些操作方法的作用是在基本数据类型、包装类对象、字符串之间提供相互之间的转化!)。
包装类的使用:
public class Test {
/** 测试Integer的用法,其他包装类与Integer类似 */
void testInteger() {
// 基本类型转化成Integer对象
Integer int1 = new Integer(10);
Integer int2 = Integer.valueOf(20); // 官方推荐这种写法
// Integer对象转化成int
int a = int1.intValue();
// 字符串转化成Integer对象
Integer int3 = Integer.parseInt("334");
Integer int4 = new Integer("999");
// Integer对象转化成字符串
String str1 = int3.toString();
// 一些常见int类型相关的常量
System.out.println("int能表示的最大整数:" + Integer.MAX_VALUE);
}
public static void main(String[] args) {
Test test = new Test();
test.testInteger();
}
}
自动装箱和拆箱就是将基本数据类型和包装类之间进行自动的互相转换。JDK1.5后,Java引入了自动装箱(autoboxing)/拆箱(unboxing)。
自动装箱:
基本类型的数据处于需要对象的环境中时,会自动转为“对象”。
我们以Integer为例:在JDK1.5以前,这样的代码 Integer i = 5 是错误的,必须要通过Integer i = new Integer(5) 这样的语句来实现基本数据类型转换成包装类的过程;而在JDK1.5以后,Java提供了自动装箱的功能,因此只需Integer i = 5这样的语句就能实现基本数据类型转换成包装类,这是因为JVM为我们执行了Integer i = Integer.valueOf(5)这样的操作,这就是Java的自动装箱。
Integer i = 100;//自动装箱
//相当于编译器自动为您作以下的语法编译:
Integer i = Integer.valueOf(100);//调用的是valueOf(100),而不是new Integer(100)
自动拆箱:
每当需要一个值时,对象会自动转成基本数据类型,没必要再去显式调用intValue()、doubleValue()等转型方法。
如 Integer i = 5;int j = i; 这样的过程就是自动拆箱。
自动装箱过程是通过调用包装类的valueOf()方法实现的,而自动拆箱过程是通过调用包装类的 xxxValue()方法实现的(xxx代表对应的基本数据类型,如intValue()、doubleValue()等)。
自动装箱与拆箱的功能事实上是编译器来帮的忙,编译器在编译时依据您所编写的语法,决定是否进行装箱或拆箱动作。
Integer i = 100;
int j = i;//自动拆箱
//相当于编译器自动为您作以下的语法编译:
int j = i.intValue();
包装类空指针异常问题:
public class Test1 {
public static void main(String[] args) {
Integer i = null;
int j = i;
}
}
执行结果如图所示:
运行结果之所以会出现空指针异常,是因为示例中的代码相当于:
public class Test1 {
public static void main(String[] args) {
//示例的代码在编译时期是合法的,但是在运行时期会有错误,因为其相当于:
Integer i = null;
int j = i.intValue();
}
}
null表示i没有指向任何对象的实体,但作为对象名称是合法的(不管这个对象名称存是否指向了某个对象的实体)。由于实际上i并没有指向任何对象的实体,所以也就不可能操作intValue()方法,这样上面的写法在运行时就会出现NullPointerException错误。
可以增加if(i!=null)的判断来避免空指针异常。
整型、char类型所对应的包装类,在自动装箱时,对于-128~127之间的值会进行缓存处理,其目的是提高效率。
缓存处理的原理为:如果数据在-128~127这个区间,那么在类加载时就已经为该区间的每个数值创建了对象,并将这256个对象存放到一个名为cache的数组中。每当自动装箱过程发生时(或者手动调用valueOf()时),就会先判断数据是否在该区间,如果在则直接获取数组中对应的包装类对象的引用,如果不在该区间,则会通过new调用包装类的构造方法来创建新对象。
Integer类的valueOf相关源码如下:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
其中:
IntegerCache类为Integer类的一个静态内部类,仅供Integer类使用。
一般情况下 IntegerCache.low为-128,IntegerCache.high为127,IntegerCache.cache为内部类的一个静态属性
当值在[-128,127]范围内时,不会去new新建Integer对象,而且去已经在缓存中创建的对象中取,这样可以提高效率。
public class test {
public static void main(String[] args) {
Integer a1=Integer.valueOf(10);
Integer a2=10;
System.out.println(a1==a2);//true,值在范围内不创建新对象
Integer a3=129;
Integer a4=129;
System.out.println(a3==a4);//false
}
}
所以对于-128~127之间的值会进行缓存处理。超过范围后,对象之间不能再使用==进行数值的比较,而是使用equals方法。
String 类对象代表不可变的Unicode字符序列,因此我们可以将String对象称为“不可变对象”。
不可变指的是对象内部的成员变量的值无法再改变。
重新赋值也只是将变量引用地址指向了新值,而原值还存在并未改变。
String的某些方法,比如:substring()是对字符串的截取操作,但本质是读取原字符串内容生成了新的字符串,原字符串并没有发生改变。
可以用==比较是否是同一个对象,equals方法比较值是否相等。
String类常用的方法有x:
1. String类的下述方法能创建并返回一个新的String对象: concat()、 replace()、substring()、 toLowerCase()、 toUpperCase()、trim()。
2. 提供查找功能的有关方法: endsWith()、 startsWith()、 indexOf()、lastIndexOf()。
3. 提供比较功能的方法: equals()、equalsIgnoreCase()、compareTo()。
4. 其它方法: charAt() 、length()。
StringBuffer和StringBuilder非常类似,均代表可变的字符序列。 这两个类都是抽象类AbstractStringBuilder的子类,方法几乎一模一样。
StringBuffer JDK1.0版本提供的类,线程安全,做线程同步检查, 效率较低。
StringBuilder JDK1.5版本提供的类,线程不安全,不做线程同步检查,因此效率较高。 建议采用该类。
public class test {
public static void main(String[] args) {
StringBuilder sb=new StringBuilder("ABCD");
System.out.println(Integer.toHexString(sb.hashCode()));
System.out.println(sb);
sb.setCharAt(1,'Q');//修改sb的值,将索引为1的值替换为Q
System.out.println(Integer.toHexString(sb.hashCode()));
System.out.println(sb);
}
}
运行结果:
4554617c ABCD 4554617c AQCD
根据结果可以看出,虽然值发生了变化,但是对象为同一个对象。
常用方法列表:
可以为该StringBuilder 对象添加字符序列,仍然返回自身对象。
可以删除从start开始到end-1为止的一段字符序列,仍然返回自身对象。
移除此序列指定位置上的 char,仍然返回自身对象。
可以为该StringBuilder 对象在指定位置插入字符序列,仍然返回自身对象。
用于将字符序列逆序,仍然返回自身对象。
方法 public String toString() 返回此序列中数据的字符串表示形式。
和 String 类含义类似的方法:
public int indexOf(String str)
public int indexOf(String str,int fromIndex)
public String substring(int start)
public String substring(int start,int end)
public int length()
char charAt(int index)
StringBuffer/StringBuilder基本用法:
public class TestStringBufferAndBuilder 1{
public static void main(String[] args) {
/**StringBuilder*/
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 7; i++) {
sb.append((char) ('a' + i));//追加单个字符
}
System.out.println(sb.toString());//转换成String输出
sb.append(", I can sing my abc!");//追加字符串
System.out.println(sb.toString());
/**StringBuffer*/
StringBuffer sb2 = new StringBuffer("中华人民共和国");
sb2.insert(0, "爱").insert(0, "我");//插入字符串,可链式操作,因为都返回同一个对象
System.out.println(sb2);
sb2.delete(0, 2);//删除子字符串
System.out.println(sb2);
sb2.deleteCharAt(0).deleteCharAt(0);//删除某个字符
System.out.println(sb2.charAt(0));//获取某个字符
System.out.println(sb2.reverse());//字符串逆序
}
}
运行结果:
String使用的陷阱:
String一经初始化后,就不会再改变其内容了。对String字符串的操作实际上是对其副本(原始拷贝)的操作,原来的字符串一点都没有改变。
比如: String s ="a"; 创建了一个字符串, s = s+"b"; 实际上原来的"a"字符串对象已经丢弃了,现在又产生了另一个字符串s+"b"(也就是"ab")。 如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的时间和空间性能,甚至会造成服务器的崩溃。
相反,StringBuilder和StringBuffer类是对原字符串本身操作的,可以对字符串进行修改而不产生副本拷贝或者产生少量的副本。因此可以在循环中使用。
String和StringBuilder在频繁字符串修改时效率测试:
public class Test {
public static void main(String[] args) {
/**使用String进行字符串的拼接*/
String str8 = "";
//本质上使用StringBuilder拼接, 但是每次循环都会生成一个StringBuilder对象
long num1 = Runtime.getRuntime().freeMemory();//获取系统剩余内存空间
long time1 = System.currentTimeMillis();//获取系统的当前时间
for (int i = 0; i < 5000; i++) {
str8 = str8 + i;//相当于产生了10000个对象
}
long num2 = Runtime.getRuntime().freeMemory();
long time2 = System.currentTimeMillis();
System.out.println("String占用内存 : " + (num1 - num2));
System.out.println("String占用时间 : " + (time2 - time1));
/**使用StringBuilder进行字符串的拼接*/
StringBuilder sb1 = new StringBuilder("");
long num3 = Runtime.getRuntime().freeMemory();
long time3 = System.currentTimeMillis();
for (int i = 0; i < 5000; i++) {
sb1.append(i);
}
long num4 = Runtime.getRuntime().freeMemory();
long time4 = System.currentTimeMillis();
System.out.println("StringBuilder占用内存 : " + (num3 - num4));
System.out.println("StringBuilder占用时间 : " + (time4 - time3));
}
}
运行结果:
long now = System.currentTimeMillis();//获取当前系统时间的时间戳
这个long类型的“时刻数值”是所有时间类的核心值,年月日都是根据这个“数值”计算出来的
1970 年 1 月 1 日 00:00:00定为基准时间
import java.util.Date;
public class test20 {
public static void main(String[] args) {
Date date1 = new Date();
System.out.println(date1.toString());//把Date对象转换为日期形式的String
long i = date1.getTime();//获取Date对象的时间戳
System.out.println(i);
Date date2 = new Date(i - 1000);
Date date3 = new Date(i + 1000);
System.out.println(date1.after(date2));//true,判断date1是否在date2之后
System.out.println(date1.before(date2));//false,判断date1是否在date2之前
System.out.println(date1.equals(date2));//false,比较两个日期的相等性
System.out.println(date1.after(date3));//false
System.out.println(date1.before(date3));//true
System.out.println(new Date(1000L * 60 * 60 * 24 * 365 * 39L).toString());
}
}
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class test21 {
public static void main(String[] args) throws ParseException {
// new出SimpleDateFormat对象,指定时间格式
SimpleDateFormat s1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
SimpleDateFormat s2 = new SimpleDateFormat("yyyy-MM-dd");
// 将时间对象转换成字符串
String daytime = s1.format(new Date());
System.out.println(daytime);
System.out.println(s2.format(new Date()));
System.out.println(new SimpleDateFormat("hh:mm:ss").format(new Date()));
// 将符合指定格式的字符串转成成时间对象.字符串格式需要和指定格式一致。
String time = "2019-04-18";
Date date = s2.parse(time);
System.out.println("date1: " + date);
time = "2019-10-7 20:15:30";
date = s1.parse(time);
System.out.println("date2: " + date);
}
}
运行结果:
2019-12-17 09:36:44 2019-12-17 09:36:44 date1: Thu Apr 18 00:00:00 CST 2019 date2: Mon Oct 07 20:15:30 CST 2019
Calendar 类是一个抽象类,为我们提供了关于日期计算的相关功能,比如:年、月、日、时、分、秒的展示和计算。
GregorianCalendar 是 Calendar 的一个具体子类,提供了世界上大多数国家/地区使用的标准日历系统。
注意:
月份的表示,一月是0,二月是1,以此类推,12月是11。 因为大多数人习惯于使用单词而不是使用数字来表示月份,这样程序也许更易读,父类Calendar使用常量来表示月份:JANUARY、FEBRUARY等等。
星期几的表示,星期天是1,星期一是2,以此类推
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
public class test22 {
public static void main(String[] args) {
// 得到相关日期元素
GregorianCalendar calendar = new GregorianCalendar(1999, 10, 9, 22, 10, 50);
int year = calendar.get(Calendar.YEAR); // 获取年,打印:1999
int month = calendar.get(Calendar.MONTH); // 获取月,打印:10,表示9月
// 日:Calendar.DATE和Calendar.DAY_OF_MONTH同义
int day = calendar.get(Calendar.DAY_OF_MONTH); // 获取日,打印:9
int day2 = calendar.get(Calendar.DATE); // 获取日,打印:9
// 星期几 这里是:1-7.周日是1,周一是2,。。。周六是7
int date = calendar.get(Calendar.DAY_OF_WEEK); // 打印:3,表示星期四
System.out.println(year);
System.out.println(month);
System.out.println(day);
System.out.println(day2);
System.out.println(date);
// 设置日期
GregorianCalendar calendar2 = new GregorianCalendar();
calendar2.set(Calendar.YEAR, 2999);
calendar2.set(Calendar.MONTH, Calendar.FEBRUARY); // 月份数:0-11
calendar2.set(Calendar.DATE, 3);
calendar2.set(Calendar.HOUR_OF_DAY, 10);
calendar2.set(Calendar.MINUTE, 20);
calendar2.set(Calendar.SECOND, 23);
printCalendar(calendar2);//自定义打印时间类
// 日期计算
GregorianCalendar calendar3 = new GregorianCalendar(2999, 10, 9, 22, 10, 50);
calendar3.add(Calendar.MONTH, -7); // 月份减7
calendar3.add(Calendar.DATE, 7); // 增加7天
printCalendar(calendar3);
// 日历对象和时间对象转化
Date d = calendar3.getTime();
GregorianCalendar calendar4 = new GregorianCalendar();
calendar4.setTime(new Date());
long g = System.currentTimeMillis();
}
static void printCalendar(Calendar calendar) {
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1;
int day = calendar.get(Calendar.DAY_OF_MONTH);
int date = calendar.get(Calendar.DAY_OF_WEEK) - 1; // 星期几
String week = "" + ((date == 0) ? "日" : date);
int hour = calendar.get(Calendar.HOUR);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
System.out.printf("%d年%d月%d日,星期%s %d:%d:%d\n", year, month, day,
week, hour, minute, second);
}
}
java.lang.Math提供了一系列静态方法用于科学计算;其方法的参数和返回值类型一般为double型。如果需要更加强大的数学运算能力,计算高等数学中的相关内容,可以使用apache commons下面的Math类库。
Math类的常用方法:
1. abs 绝对值
2. acos,asin,atan,cos,sin,tan 三角函数
3. sqrt 平方根
4. pow(double a, double b) a的b次幂
5. max(double a, double b) 取大值
6. min(double a, double b) 取小值
7. ceil(double a) 大于a的最小整数
8. floor(double a) 小于a的最大整数
9. random() 返回 0.0 到 1.0 的随机数
10. long round(double a) double型的数据a转换为long型(四舍五入)
11. toDegrees(double angrad) 弧度->角度
12. toRadians(double angdeg) 角度->弧度
public class TestMath {
public static void main(String[] args) {
//取整相关操作
System.out.println(Math.ceil(3.2));//输出大于3.2的最小整数,4.0
System.out.println(Math.floor(3.2));//输出小于3.2的最小整数,3.0
System.out.println(Math.round(3.2));//四舍五入,3
System.out.println(Math.round(3.8));//四舍五入,4
//绝对值、开方、a的b次幂等操作
System.out.println(Math.abs(-45));//取绝对值,45
System.out.println(Math.sqrt(64));//平方根,8.0
System.out.println(Math.pow(5, 2));//5的2次幂,25.0
System.out.println(Math.pow(2, 5));//2的5次幂,32.0
//Math类中常用的常量
System.out.println(Math.PI);//3.141592653589793
System.out.println(Math.E);//2.718281828459045
//随机数
System.out.println(Math.random());//返回0.0到1.0的随机数[0,1)
}
}
Random类的常用方法:
Random类,专门用来生成随机数的类;Math.random()底层调用的就是Random的nextDouble()方法。
import java.util.Random;
public class TestRandom {
public static void main(String[] args) {
Random rand = new Random();
//随机生成[0,1)之间的double类型的数据
System.out.println(rand.nextDouble());
//随机生成int类型允许范围之内的整型数据
System.out.println(rand.nextInt());
//随机生成[0,1)之间的float类型的数据
System.out.println(rand.nextFloat());
//随机生成false或者true
System.out.println(rand.nextBoolean());
//随机生成[0,10)之间的int类型的数据
System.out.print(rand.nextInt(10));
//随机生成[20,30)之间的int类型的数据
System.out.print(20 + rand.nextInt(10));
//随机生成[20,30)之间的int类型的数据(此种方法计算较为复杂)
System.out.print(20 + (int) (rand.nextDouble() * 10));
}
}
Random类位于java.util包下。
java.io.File类:代表文件和目录。 在开发中,读取文件、生成文件、删除文件、修改文件的属性时经常会用到本类。
File类的常见构造方法:public File(String pathname)
以pathname为路径创建File对象,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储。
文件的创建:
import java.io.File;
public class TestFile1 {
public static void main(String[] args) throws Exception {
//user.dir为当前项目的路径
System.out.println(System.getProperty("user.dir"));
File f = new File("a.txt"); //相对路径:默认放到user.dir目录下面
f.createNewFile();//创建文件
File f2 = new File("d:/b.txt");//绝对路径
f2.createNewFile();
}
}
File类访问属性的方法列表:
测试File类访问属性的基本用法:
import java.io.File;
import java.util.Date;
public class TestFile2 {
public static void main(String[] args) throws Exception {
File f = new File("d:/b.txt");
System.out.println("File是否存在:"+f.exists());
System.out.println("File是否是目录:"+f.isDirectory());
System.out.println("File是否是文件:"+f.isFile());
System.out.println("File最后修改时间:"+new Date(f.lastModified()));
System.out.println("File的大小:"+f.length());
System.out.println("File的文件名:"+f.getName());
System.out.println("File的目录路径:"+f.getPath());
}
}
运行结果:
File是否存在:true File是否是目录:false File是否是文件:true File最后修改时间:Tue Dec 17 22:30:01 CST 2019 File的大小:0 File的文件名:b.txt File的目录路径:e:\b.txt
通过File对象创建空文件或目录(在该对象所指的文件或目录不存在的情况下):
使用mkdir创建目录:
import java.io.File;
public class TestFile3 {
public static void main(String[] args) throws Exception {
File f = new File("d:/c.txt");
f.createNewFile(); // 会在d盘下面生成c.txt文件
f.delete(); // 将该文件或目录从硬盘上删除
File f2 = new File("d:/电影/华语/大陆");
boolean flag = f2.mkdir(); //d:/电影/华语/目录结构中有一个不存在,则不会创建整个目录树
System.out.println(flag);//创建失败
}
}
使用mkdirs创建目录:
import java.io.File;
public class TestFile4 {
public static void main(String[] args) throws Exception {
File f = new File("d:/c.txt");
f.createNewFile(); // 会在d盘下面生成c.txt文件
f.delete(); // 将该文件或目录从硬盘上删除
File f2 = new File("d:/电影/华语/大陆");
boolean flag = f2.mkdirs();//目录结构中有一个不存在也没关系;创建整个目录树
System.out.println(flag);//创建成功
}
}
File类的综合应用:
import java.io.File;
import java.io.IOException;
public class TestFile5 {
public static void main(String[] args) {
//指定一个文件
File file = new File("d:/test/b.txt");
//判断该文件是否存在
boolean flag= file.exists();
//如果存在就删除,如果不存在就创建
if(flag){
//删除
boolean flagd = file.delete();
if(flagd){
System.out.println("删除成功");
}else{
System.out.println("删除失败");
}
}else{
//创建
boolean flagn = true;
try {
//如果目录不存在,先创建目录
File dir = file.getParentFile();
dir.mkdirs();
//创建文件
flagn = file.createNewFile();
System.out.println("创建成功");
} catch (IOException e) {
System.out.println("创建失败");
e.printStackTrace();
}
}
//文件重命名
//file.renameTo(new File("d:/readme.txt"));
}
}
递归遍历目录结构和树状展现:
使用递归算法,以树状结构展示目录树和目录内的文件:
import java.io.File;
public class TestFile6 {
public static void main(String[] args) {
File f = new File("d:/电影");//指定一个目录位置
printFile(f, 0);
}
/**
* 打印文件信息
* @param file 文件名称
* @param level 层次数(实际就是:第几次递归调用)
*/
static void printFile(File file, int level) {
//输出层次数
for (int i = 0; i < level; i++) {
System.out.print("-");
}
//输出文件名
System.out.println(file.getName());
//如果file是目录,则获取子文件列表,并对每个子文件进行相同的操作
if (file.isDirectory()) {
File[] files = file.listFiles();
for (File temp : files) {
//递归调用该方法:注意层次+1
printFile(temp, level + 1);
}
}
}
}
JDK1.5引入了枚举类型。枚举类型的定义包括枚举声明和枚举体。格式如下:
enum 枚举名 {
枚举体(常量列表)
}
枚举体就是放置一些常量。
当你需要定义一组常量时,可以使用枚举类型。
尽量不要使用枚举的高级特性,事实上高级特性都可以使用普通类来实现,没有必要引入枚举,增加程序的复杂性!
创建枚举类型:
enum Season {
SPRING, SUMMER, AUTUMN, WINDER
}
所有的枚举类型隐性地继承自 java.lang.Enum。枚举实质上还是类!而每个被枚举的成员实质就是一个枚举类型的实例,他们默认都是public static final修饰的。可以直接通过枚举类型名使用它们。
枚举的使用:
import java.util.Random;
public class TestEnum {
public static void main(String[] args) {
// 枚举遍历
for (Week k : Week.values()) {
System.out.println(k);
}
// switch语句中使用枚举
int a = new Random().nextInt(4); // 生成0,1,2,3的随机数
switch (Season.values()[a]) {
case SPRING:
System.out.println("春天");
break;
case SUMMER:
System.out.println("夏天");
break;
case AUTUMN:
System.out.println("秋天");
break;
case WINDTER:
System.out.println("冬天");
break;
}
}
}
/**季节*/
enum Season {
SPRING, SUMMER, AUTUMN, WINDTER
}
/**星期*/
enum Week {
星期一, 星期二, 星期三, 星期四, 星期五, 星期六, 星期日
}
评论