《Java语言程序设计》

一、Java概述 

        Java是简单的,面向对象的语言,具有分布性、安全性、健壮性;最初版本是解释执行的,后期增加了编译执行;是多线程的、动态的;最主要的是与平台无关,解决了软件移植的问题。

Java语言特点

  • 语法简单,功能强大,安全可靠(没有指针,无多重继承机制,具有自动内存回收机制,强类型语言,在语言定义、字节码检查、程序执行阶段实行三级代码安全检查机制),通过对象的封装、类的继承,方法的多态实现了代码的复用、信息隐藏、动态绑定的特性。
  • 与平台无关:在实际的计算机上仿真模拟各种计算机功能,不同操作系统有不同的虚拟机。一般的语言需要针对指令而编译成不同的目标文件,但是JVM是将程序编译成虚拟机可以识别的二进制代码,也就是字节码,只运行在JVM上,JVM执行字节码文件时,把字节码解释成具体平台上的机器指令执行,所以不需要编译。
  • 解释编译两种运行方式
  • 支持多线程
  • 动态执行并有丰富的API文档类库

面向对象的技术

  • OOA:面向对象的分析
  • OOD:面向对象的设计
  • OOP:面向对象的程序设计

类的概念:来自于同一原型,具有同一样的共性。对象是类的一个具象,类是对象的一个抽象。

三大技术

  • 封装:是将对象的属性及实现细节隐藏起来,只给出如何使用的信息
  • 继承:将已有的一个类的数据和方法保留,并加上自己特殊数据和方法,构成一个新类。体现的是一种层次关系,下一层的类可从上一层的类继承定义。
  • 多态:可以让多个方法使用同一个名字,可以保证对不同类型的数据进行等同的操作;使用相同的操作名,能根据具体的对象自动选择相应的操作。

二、数据和表达式

语句是Java程序最小的执行单位。

1、标识符:由字母、数字、下划线_和美元符号$组成的字符串,但是不可以数字开头,也不可以包含其他的符号,不允许插入空白。

2、Java源代码是使用的Unicode码,不是ASCII码。Unicode码是16位无符号二进制数,可多达65535个,比通常的ASCII码255个字符大得多。Unicode兼容了许多不同的字母表,汉字也是字符,所以汉字也可以是当做标识符使用。

3、数据类型

  • 基本数据类型:整型、浮点型、字符型、布尔型
  • 复合数据类型:数组、类、接口

4、整型类型的长度:8,16,32,64;字节数:1,2,4,8

5、进制:1~9是十进制,0开头是八进制,0x或0X开头的是十六进制

6、浮点类型的默认为double类型的;单个字符用char类型表示,一个char表示一个Unicode字符;boolean类型默认为false,计算机内部使用8位二进制表示

7、表达式由运算符和操作数组成。六种运算符:算术、逻辑、关系、位、赋值、条件

8、变量声明:方法内定义的变量称为局部变量、临时变量或栈变量;类中定义的变量就是类的成员变量

9、简单数据类型变量声明后:系统自动在内存分配对应的存储空间。而引用类型的,系统只分配引用空间,程序员需要使用new来创建实例,才可分配相应的存储空间。具有null值得引用不指向任何对象。

10、Java不允许将未初始化的变量进行操作,如果变量声明时没有初始化,那么在使用该变量前必须要进行赋值初始化。

11、许多语言的取模运算只能用于整型数,Java还可以允许对浮点数的取模。

12、有两个操作数的运算符是二元运算符,一个操作数的是一元运算符。

13、位运算符

        位运算符比一般的算术运算符速度要快,而且可以实现一些算术运算符不能实现的功能,位运算符用来对二进制位进行操作,包括:按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、按位左移(<<)、按位右移(>>)。加减乘除适用于十进制,而位运算就是二进制的运算

  • 按位与(&)

如果  4&7   那么这个应该怎么运算呢?首先我们需要把两个十进制的数转换成二进制,4=00000100,7=00000111

在这里要提到一点,1表示true,0表示false,而与运算的时候相同位之间其实就是两个Boolean的运算,全true(1),即为true(1) ,全false(0),即为false(0),一false(0)一true(1),还是false(0)

  • 按位或(|)

以5|9为例,5=00000101,9=00001001,

做与运算的时候,遇true(1)就是true(1),无true(1)就是false(0)

  • 按位异或(^)

 以 7^15 为例,7=00000111,15=00001111

在异或的时候,只要相同都是false(0),只有不同才是true(1)

  • 按位取反(~)

 ~15,同样的先变成二进制:15=00001111

这个其实挺简单的,就是把1变0,0变1

注意:二进制中,最高位是符号位   1表示负数,0表示正数

  • 按位左移(<<)

左移就是把所有位向左移动几位,12 << 2 意思就是12向左移动两位,12=00001100

通过这个图我们可以看出来,所有的位全都向左移动两位,然后把右边空的两个位用0补上,最左边多出的两个位去掉,最后得到的结果就是00110000  结果就是48,我们用同样的办法算12<<3结果是96,8<<4结果是128,由此我们得出一个快速的算法,M<<n,其实可以这么算M<<n=M*2n

  • 按位右移(>>)

这个跟左移运算大体是一样的,12 >> 2

我们可以看出来右移和左移其实是一样的,但是还是有点不同的,不同点在于对于正数和负数补位的时候补的不一样,负数补1,正数补0,如我们再做一个–8的    -8>>2

这里总结一下,关于负数或者正数来说,移位的时候是一样的,但是在补位的时候,如果最高位是0就补0,如果最高位是1就补1,由此我们得出一个快速的算法,M>>n,其实可以这么算:M>>n=M/2n

  • 无符号右移(>>>)

        无符号右移(>>>)只对32位和64位有意义,在移动位的时候与右移运算符的移动方式一样的,区别只在于补位的时候不管是0还是1,都补0

总结

  • 按位与:全true(1),即为true(1) ,全false(0),即为false(0),一false(0)一true(1),还是false(0)
  • 按位或:遇true(1)就是true(1),无true(1)就是false(0)
  • 异或:只要相同都是false(0),只有不同才是true(1)
  • 取反:就是把1变0,0变1
  • 左移:M<<n=M*2n
  • 右移:M>>n=M/2n
  • 无符号右移:与右移一样,但是补位的时候不管是0还是1,都补0

        Java是一种强类型的语言,不支持变量类型的自动任意转换,转换的原则:(一个字节是8位)位数少的类型转换为位数多的类型,称为自动类型转换。能够自动进行类型转换的类型顺序:byte、short、char、int、long、float、double(知道每个类型的长度和字节数)

注意:虽然int类型与float类型占用的位数一样多,long类型与double类型占用的位数是一样多,但由于浮点数表示的数的范围远远大于整型数,所以,int和long向float或double转换是可以自动进行的,但可能会丢失精度

当位数多的向位数少的转换时,需要显示的转换,手动转换,这种叫做强制类型转换。

进制转换(二进制、十进制互转)

十进制转二进制(正整数转二进制,负整数转二进制,小数转二进制)

  • 正整数转成二进制:除二取余,然后倒序排列,注意是倒序排列,高位补零。比如42转二进制,42除以2得到的余数分别为010101,然后倒着排一下,42所对应二进制就是101010,计算机内部表示数的字节单位是定长的,如8位,16位,或32位。所以,位数不够时,高位补零,所以,42转换成二进制以后就是:00101010
  • 负整数转二进制:先是将对应的正整数转换成二进制后,对二进制取反,然后对结果再加一。还以42为例,负整数就是-42。正整数42转换成二进制以后就是:00101010,取反后就是11010101,结果加一就是:-42=11010110,(因为二进制的表达方式只有0和1,满2进1,个位的1和1相加就会进一位然后归零,十位与个位进的1相加也为2 进1也归零)
  • 小数转二进制:对小数点以后的数乘以2,有一个结果吧,取结果的整数部分(不是1就是0喽),然后再用小数部分再乘以2,再取结果的整数部分……以此类推,直到小数部分为0或者位数已经够了就OK了。然后把取的整数部分按先后次序排列,就构成了二进制小数部分的序列,比如0.125,0.125*2=0.25,整数为0,0.25*2=0.5,整数为0,0.5*2=1,整数位1,好了,没有小数了,那么就不乘2了,正序排列构成了二进制的小数部分,0.001,如果小数的整数部分大于0,那么就把整数转换成二进制,小数转换成二进制,然后加在一起就OK了,比如6.125的二进制就是110.001

二进制转十进制

  • 首先将二进制数补齐位数,首位如果是0就代表是正整数,如果首位是1则代表是负整数。
  • 若二进制补足位数后首位是0的正整数,补齐位数以后,将二进制中的位数分别将下边对应的值相乘,然后相加得到的就为十进制,比如1010补齐位数是00001010,转换为十进制,0*2^0=0,1*2^1=2,0*2^2=0,1*2^3=8,所以1010的二进制就是0+2+0+8=10
  • 若二进制补足位数后首位为1时,就需要先取反再换算:例如,11101011,首位为1,那么就先取反吧:-00010100,然后算一下10100对应的十进制为20,所以对应的十进制为-20
  • 将有小数的二进制转换为十进制时:例如0.1101转换为十进制的方法:将二进制中的四位数分别于下边对应的值相乘后相加得到的值即为换算后的十进制。0*2^0=0,1*2^-1=0.5,1*2^-2=0.25,0*2^-3=0,1*2^-4=0.0625,所以0.1101的十进制为0+0.5+0.25+0+0.0625=0.8125

流程控制语句

Java程序结构

  • package语句,每个语句只能有一个,必须放在文件最开始的地方
  • import语句,可以没有,也可以有多个,如果有,就必须放在所有类定义的前面

赋值语句、分支语句和循环语句分别对应了三类语句流,顺序流、分支流和循环流

分支语句由if和switch语句组成,循环语句有for,while,do while语句组成。

String str = "1";
//表达式的类型可以是int、char、string
switch (str){
    //switch语句的判断也可以这样写
    case "6":case "7":
    break;
}
//无限for循环也可以这么写
for(;;){
    System.out.println("循环");
}
//或这么写
for(;true;){
    System.out.println("循环");
}

2、break关键字是用来跳过余下的语句,结束循环的,continue关键字是用来立即结束当次循环,开始执行下一次循环的。

3、Scanner对象是用空白、水平制表符以及回车符作为输入的分割元素,也就是结束的标志

4、Java中把错误分为两类

  • 非致命性的:通过修改程序后可以修复的bug,比如数组越界,除零异常,文件不存在等等
  • 致命性的:无法通过修改程序来进行修复,比如内存溢出

5、异常分类:Exception类是所有异常类的父类,Error类是所有错误类的父类,同时这两个类又是Throwable类的子类。

6、异常分为3种

  • 受检异常(必须被处理):程序执行期间发生的严重事件的后果。文件不存在,IO异常,类不存在等。受检异常的所有类是类Exception的子类,Exception是Throwable的后代。
  • 运行时异常(不需要处理):程序中逻辑错误的结果。数据下标越界,除零等。运行时异常的所有类都是类RuntimeException的子类,它是Exception的后代。
  • 错误(不需要处理):是Error类或后代类的对象,Error是Throwable的后代。内存溢出

简单来说,分为两大类,受检异常不检异常

7、只有在try或catch语句块中,执行System.exit();才会不执行finally块中的语句,这是不执行finally语句的唯一一种可能。

8、在方法体中用保留字throw抛出一个异常,在方法头中用保留字throws来声明这个方法可能抛出的异常。

四、面向对象程序设计

1、类中含有两部分元素:成员变量成员方法

2、访问关系和访问权限关键字的关系

类型无修饰符privateprotectedpublic
同一类
同一包中的子类
同一包中的非子类
不同包中的子类
不同包中的非子类

3、类定义中可以指明父类,也可以不指明。若没有指明是哪个类派生,则表明是默认的父类Object类而来,Object是所有类的直接或间接父类,Object类是唯一没有父类的类。

4、构造方法,名字与类名相同,没有返回值,在创建对象实例时通过new运算符自动调用,一个类可以有不同参数列表的构造方法,可以重载。构造方法不能声明为native、abstract、synchronized或final类型。构造方法不能从父类继承。每个类至少有一个构造方法,如果程序没有定义,系统会默认生成一个该类的无参构造方法,如果类中已经声明,则系统不会再声明。

5、可以用this关键字来指代本类中的其他构造方法。

public class Student{
    String name;
    int age;
    //第一个构造方法
    public Student(String name,int age){
        this.name = name;
        this.age = age;
    }
    //第二个构造方法
    public Student(String name){
        this(name,20);
    }
    //第三个构造方法
    public Student(){
        this("Unknown");
    }
}

6、类的定义就像一个“模子”,声明一个个类类型变量的过程就像是复制了一个个的副本。

        变量声明后,在内存中建立起了一个引用,但不指向任何内存空间,需要使用new申请对应的内存空间/存储空间。并将该内存的首地址赋给刚才的引用。换句话说,用class类型声明的变量并不是数据本身,而是对数据的引用,要用new关键字来进一步创建类的实例或对象本身。

7、当通过new为一个对象分配内存时,如果构造方法没有为成员变量赋初值,则Java进行自动初始化。对于数值变量,初始化为0;对于布尔变量,初始化为false;对于引用类型,初始化为null

8、引用变量的赋值和基本数据类型变量的赋值

public void demo2(){
    //基本数据类型
    int a =10;
    int b = a;
    System.out.println(a+"-"+b);
    a=11;
    System.out.println(a+"-"+b);
    //引用数据类型
    String x = "hello";
    String y = x;
    System.out.println(x+"-"+y);
    x = "world";
    System.out.println(x+"-"+y);
}

输出为:
10-10
11-10
hello-hello
world-hello

        a和b都是独立变量,对一个变量的修改不会影响到另一个变量,所以修改后a=11,b=10。但是对于引用类型来说,对变量x和y,只存在一个String对象,就是“hello”,x和y都指向了这个对象,但是因为String类型的不可修改的特性,x="world",不是因为x的原来的值“hello”该为“world”,只是将a指向“hello”,该为了指向“world”,但是y还是指向“hello”的,所以结果为world-hello

9、调用方法时,传给方法的值称为实参,方法参数列表的值称为形参

10、按值传递,如果形参是基本数据类型的,则调用方法时,将实参的“值”复制给形参,返回时,在方法内对形参的任何修改,都不会影响实参的值。如果形参是引用类型,则调用方法时,传递给形参的则是一个地址值,返回时地址值不会改变,但是地址中的内容可以被修改。

11、 重载:允许多个方法使用同一个方法名,但是参数列表不同(参数的个数,类型,顺序),与返回值类型无关

12、用static修饰的被称为静态成员或类成员。如果一个类中包含了静态成员,系统只在类定义时为静态成员分配内存,此时还没有创建对象,也没有对对象进行实例化。以后生成该类的实例对象时,不再为静态成员分配内存,不同对象的静态变量将共享同一块内存空间。Person p1 = new Persion();Person p2 = new Persion();Person类中的静态变量,p1和p2共享同一块

public class staticC {
    private int num;
    static int counter = 0;

    public staticC(){
        counter++;
        num = counter;
    }
    public int getNum(){
        return counter;
    }
}

//测试
@Test
public void test10() {
    System.out.println(staticC.counter);
    staticC staticC1 = new staticC();
    staticC staticC2 = new staticC();
    System.out.println(staticC1.getNum());
    System.out.println(staticC2.getNum());
    System.out.println(staticC.counter);
}
输出的是:0,2,2,2,

由上述例子可证,每一个被创建的对象都得到唯一一个的counter,该值由0开始递增,当一个对象的构造方法将其递增后,下一个将要被创建的对象所看到的counter值就是递增之后的值。

13、静态方法:用static修饰的方法。非静态方法被称为实例方法

  • 由于静态方法可以在没有创建类实例情况下调用,所以不存在this值,并且静态方法只能调用静态变量或方法,不能调用非静态成员
  • 静态方法不能被重写
  • 用static修饰的方法必须要有方法体,不能只有声明,也就是说接口中的方法不能用static修饰,抽象方法也不能被static修饰

14、自动装箱:自动将基本数据类型转换为对应的包装类的过程。自动拆箱:自动将包装类转换为对应的基本数据类型的过程。

自动装箱和拆箱仅能用在基本数据类型与对应的包装类之间

五、数组和字符串

1、数组定义:是具有相同数据类型的元素按一定顺序排列,连续存储的集合。

2、数组在定义时并不会为数组分配内存,声明并不代表创建数组对象本身,声明的数组名只是引用变量,用来指向一个数组。

3、数组的初始化才是数组的创建,分为静态初始化和动态初始化。静态初始化指在定义数组的同时给元素赋值。动态初始化指使用运算符new给数组分配空间。

4、对于基本数据类型的数组,使用new运算符不仅创建了内存空间,还初始化了,因为基本数据类型,有默认值。而对于引用数据类型的数组来说,使用new运算符只是为数组本身分配了空间,并没有对数组的元素进行初始化。所以引用类型的数组,空间分配分为两步

  • 先创建数组本身
  • 分别创建各个数组元素

就如下图,应该这么创建:

str[0]=new String();
str[1]=new String();
str[2]=new String();
str[3]=new String();

这下,str数组的每个下标都对应了真正的string对象,初始化赋值完成

@Test
public void test11() {
    //基本数据类型,在创建的时候已经分配了空间和元素初始化
    int[] arr = new int[4];
    for(int i=0;i<arr.length;i++){
        System.out.println(arr[i]);
    }
    //引用数据类型,只是分配了空间,但是没有对元素进行初始化,
    //也就是说只是创建了4个string类型变量的数组,但是没有创建4个string对象。
    String[] str = new String[4];
    for(int i=0;i<str.length;i++){
        System.out.println(str[i]);
    }
}

5、使用new创建数组时系统自动给length赋值。数组一旦创建完毕,其大小就被固定下来了。

String[] arr = {"1","2","3"};
arr[1]=null;
//仍然打印出3,说明length并不是计算赋值的元素个数,当你new时,是多少,就是多少,不因为值改变而改变
System.out.println(arr.length);

6、多维数组:n维数组是n-1维数组的数组。int[][]表示是二维数组,每个元素都是int类型。与一维数组一样,二维数组定义时不分配内存空间,同样进行初始化后才可以访问每个元素。

7、多维数组也为动态和静态初始化,外层括号的各元素是数组第一维的各元素,内层括号对应的是第二维的元素。使用两个下标可以访问数组中的对应元素,比如intArray[1][1]代表了该数组第2行第2列的元素。int intArray[][] = {{2,3},{1,5},{3,4}}说明了是一个3行2列的数组。

8、多维数组进行动态初始化时,有两种分配内存空间的方法:直接分配和按维分配

  • 直接分配:直接为每一维分配空间,声明数组时,给出各维的大小,例:int a[][] = new int[2][3],声明了2行3列的二维数组
  • 按维分配:必须从最高维起,分别为每一维分配内存。int a[][] = new int[2][],a[0]=new int[5];a[1]=new int[5];第一行说明创建了一个第一维大小为4的数组对象,后续两句话是指让这两个元素指向各含5个元素的一维数组,构成了一个2行5列的数组。int arr1[][] = new int[][4],这样声明是不正确的,声明顺序应从高维到低维,先说明高维,再说明低维

创建数组时第二维大小可以是不一样的,这样是创建了一个非矩阵数组。

9、二维数组也有length属性,但是只表示第一维的长度。数组创建后就不能改变其大小了,但是可以使用同一个引用变量指向另一个全新的数组,例如:int ele[] = new int[6];ele[] = new int[10];当执行到第二句时,第一个数组丢失了,除非还有其他引用指向他

10、String类是处理不可变字符(字符串一旦创建,其内容不可改变),StringBuffer类是处理可变字符串。

11、String类的对象实例是不可变的 ,对字符串施加操作后并不是改变当前字符串的本身,而是又生成了一个对象。而StringBuffer类的对象实例,施加操作后是直接操作字符串本身。

12、系统对String类对象分配内存时,按照对象中所包含的实际个数等量分配,而为StringBuffer类对象分配时,除去字符所占空间外,再另加16个字符大小的缓冲区。所以对于StringBuffer类对象来说,length()获得是字符串的长度,capacity()返回的是当前的容量,即字符串长度再加上缓冲区的大小。

13、==判断两个字符串对象是否是同一实例,也就是地址值是否一样,equals()是判断两个字符串是否相等

String s1 = "Hello";
String s2 = "Hello";
String s3 = new String("Hello");
String s4 = new String(s1);
String s5 = s1;
System.out.println(s1.equals(s2));  //true;
System.out.println(s1 == s2);       //true;相同的字符串在系统内部只存在一个,所以地址值一样
System.out.println(s1.equals(s3));  //true;
System.out.println(s1 == s3);       //false;
System.out.println(s1.equals(s4));  //true;
System.out.println(s1 == s4);       //false;
System.out.println(s1.equals(s5));  //true;
System.out.println(s1 == s5);       //true;

14、数组空间一经申请就不可再改变,但是Vector类则是可变数组,可以根据需要来改变,且保存的元素类型也可以不同。但是Vector类的实例中只能保存对象类型,而不能是基本数据类型。

Vector包含的成员变量有三个:

  • protected Object[] elementData;元素存储的数组缓冲区
  • protected int elementData;对象中元素的数量
  • protected int capacityIncrement;增量的大小,如果值为0,则缓冲区的大小每次倍增

       系统内部会记录Vector类实例的容量capacity,实际保存的元素个数由elementCount来记录,这个值不能大于容量值,当有元素加入到向量时,elementCount会相应增大。当向量中添加的元素超过了它的容量后,向量的存储空间以容量增值capacityIncrement的大小为单位增长,为以后新的元素加入做好准备,元素保存在数组elementData中。

//初始有100个字符串的空间,以后一旦空间用尽则以50为单位递增
Vector<String> myVector = new Vector<String>(100, 50);

将元素插入指定下标位置后,原来这个位置及后续各位置元素均后移一位;删除指定下标的元素后,原来这个位置及后续各位置元素均前移一位;

六、继承与多态

1、如果一个类没有出现extends关键字,则表明这个类派生于Object类

Person p1 = new Person("Jack",18);
Person p2 = new Person("Jack",18);
if(p1.equals(p2)){
    System.out.println("equals相等");
}else{
    System.out.println("equals不等");
}
if(p1==p2){
    System.out.println("==相等");
}else{
    System.out.println("==不等");
}
//==和equals都是判定两个对象是否是同一对象,注意是“同一”。同一对象一定相等,但相等对象不一定同一
//上述程序,p1和p2是两个独立的对象,所以不是同一,如果p2=p1这种赋值操作符的话,则是同一对象

2、虽然一个子类可以从父类中继承所有能继承的方法和成员变量,但它不能继承构造方法。只有两种方式能让一个类得到构造方法,一种方式是自己编写构造方法;另一种方式是,在用户没有编写构造方式时,系统默认生成无参的构造方法

3、子类不能直接访问父类中定义的私有属性及方法,但可以使用父类中定义的公有/保护方法访问私有数据成员

4、对象转型:Java允许对象的父类类型的一个变量指向该对象,也就是说将子类的对象赋给父类的变量,但不能反过来。

Manager m = new Employee();

对象引用转型的规则

  • 类层次上向“上”转型是合法的,父类 a = new 子类(),且此种转型不需要转型运算符,只使用简单的赋值语句即可
  • 对应向“下”转型,只能是祖先类转型到后代类,其他类之间是不允许的,因为这两个类没有继承关系,强行转型,会编译错误

类的变量既可以指向本类实例,又可以指向其子类的实例,表现了对象的多态性。

5、重写:子类中定义方法所用的名字、返回类型以及参数列表和父类中方法使用的完全一样,从逻辑上看就是子类中的成员方法将隐藏父类中的同名方法。注意:子类方法不能比父类方法的访问权限级别低。如果子类已经重写了父类的方法,但在子类中想调用父类中被隐藏的方法,可以使用super关键字

6、重载是发生在一个类之间,重写是发生在父子类之间

7、由继承的机制可知,super.method()所调用的方法不一定在父类中加以描述的,也可能是父类从它的祖先类中继承而来,因此,有可能需要按照继承层次关系依次向上查询才能找得到

8、出于安全性的考虑,Java对于对象的初始化要求是非常严格的,父类的对象要在子类运行前完全初始化。子类无法从父类继承构造方法。

如果在子类构造方法的定义中没有明确调用父类的构造方法,则系统在执行子类构造方法时会自动调用父类的无参构造方法

如果在子类构造方法的定义中调用的父类的构造方法,则调用语句必须出现在子类构造方法的第一行

9、多态中要执行的是与对象真正类型(运行时类型)相关的方法,而不是引用类型(编译时类型)相关的方法。

  • 变量的静态类型是声明时的类型,也称为引用类型,是在代码编译期间确定下来的。
  • 变量的动态类型是在运行过程中某一时刻指向的对象类型,是它此刻的真正类型。变量的动态类型随运行进程改变而改变

动态绑定:调用稍后可能被覆盖的方法,是到运行时才能确定要执行的方法代码

静态绑定:在编译过程中能确定调用方法的处理方式

//这里的Person就是静态类型,Employee就是动态类型
Person p1 = new Employee();
//静态类型和动态类型一致
Person p2 = new Person();
//静态类型和动态类型不一致
Person p3 = new Employee();
//虽然声明类型是父类的,但是指向的是子类的实例,所以调用的是子类的方法,而不是父类的方法
p3.method()

10、final关键字

  • 修饰类,该类不能有子类
  • 修饰方法,该方法不能被覆盖
  • 修饰变量,该变量的值不能被改变
  • 不能是修饰在抽象方法上

如果将一个引用类型的变量标记为final,那么这个变量将不能指向其他对象,但它所指向对象的属性值是可以改变

final String str = "123";
//编译期错误,已是final,不可指向别的对象
str = "456";

final Car car = new Car();
//编译通过,虽然不可以指向别的对象,但是可以修改其内容
car.number = 10;
//编译失败,已是final,不可指向别的对象
car = new Car();

11、abstract关键字

  • 修饰方法,方法体为空
  • 修饰类,该类必须被子类继承
  • 不能被static、final关键字修饰

12、Java中预定义的String类是不能被继承的,是为了保证如果一个方法有一个指向String类的引用,那么它肯定是一个真正的String类型,而不是一个已被更改的String的子类。或者当某个类的结构和功能很完整,不需要子类,那么可以用final修饰该类

13、定义了方法但没有具体实现的类称为抽象类,该方法称为抽象方法。

  • 抽象类无法被实例化
  • 抽象类中可以包含非抽象方法,但是非抽象类中不能定义抽象方法,也就是说,只有抽象类才能具有抽象方法
  • 抽象子类所继承的抽象方法同样还是抽象方法,除非有了具体实现,否则还是抽象类

14、接口是体现抽象类的另一种方式,是一种“纯”的抽象类,因为接口内的方法都是没有方法体的,只有方法声明

15、接口与一般类一样,本身也具有数据成员变量与方法,但数据成员变量一定要赋初值,且此值不可更改,而方法必须是抽象方法

16、在接口中定义的成员变量都默认为final静态变量,即系统会自动添加final和static变量这两个关键字,并且对该变量必须设置初值。Java允许省略定义数据成员的final关键字、方法的public及abstract关键字

public interface Sharpen {
    //编译异常,必须赋初值
    int i;
    //编译正常
    int a =0;
    int arr();
}

17、不能实例化接口,这点与抽象类一致,所以接口中没有构造方法,只是定义行为规范,要构造方法没有意义。但在抽象类中可以有构造方法,只是不能直接创建抽象类的实例对象,但实例化子类的时候,就会初始化父类,不管父类是不是抽象类都会调用父类的构造方法,初始化一个类,先初始化父类。 

18、接口和抽象类的区别

  • 接口不能有构造方法,抽象类可以有
  • 接口不能有方法体,抽象类可以有
  • 接口不能有静态方法,抽象类可以有
  • 在接口中凡是变量必须是public static final,而在抽象类中没有要求

七、输入和输出流

1、流:不同类型的输入、输出源。数据流:输入或输出的数据。数据流是指一组有顺序的、有起点和终点的字节集合

2、数据流分为输入数据流和输出数据流。输入数据流只能读不能写,输出数据流只能写不能读

InputStream、OutputStream:用于字节传输,二者都是抽象类,不能被实例化,使用需要调用其子类,比如:文件数据流(FileInputStream、FileOutputStream)

Reader、Writer:用于字符传输

3、最初版本中,Java只有普通的字节流,以byte为基本处理单元的流,字节流用来读写8位的数据。对数据流中字节的读取通常是按从头到尾顺序进行的,如果需要反方向读取,则需要使用回推操作。

       为提高数据传输速度,提高数据输出效率,有时输出数据流会在提交数据之前把所要输出的数据先暂时保存在内存缓冲区中,然后成批进行输出,每次输出过程都以某种限定数据长度为单位进行传输。这种方式下,数据的末尾一般都会有一部分数据由于数量不够一个批次,而存留在缓冲区里,调用flush()可以将这部分数据强制提交

4、基本字节数据流类

一、文件数据流:FileInputStream、FileOutputStream,用于进行文件的I/O操作

try {
    FileInputStream fileInputStream = new FileInputStream("myFIle");
    FileOutputStream fileOutputStream = new FileOutputStream("myFIle");
} catch (FileNotFoundException e) {
    e.printStackTrace();
}

二、过滤器数据流

  • 缓冲区数据流:BufferedInputStream、BufferedOutputStream,它们是在数据流上增加了一个缓冲区。当进行读写操作时,数据以块为单位先进入缓冲区(块的大小可以设置)其后的读写操作则操作缓冲区。采用此种方法降低不同硬件设置的速度差异,提高I/O操作效率。在关闭缓冲区输出流之前,应先使用flush(),强制输出剩余数据,确保数据的完整写出。
try {
    FileInputStream fileInputStream = new FileInputStream("myFIle");
    BufferedInputStream inputStream = new BufferedInputStream(fileInputStream);
    FileOutputStream fileOutputStream = new FileOutputStream("myFIle");
    BufferedOutputStream outputStream = new BufferedOutputStream(fileOutputStream);
} catch (FileNotFoundException e) {
    e.printStackTrace();
}
  • 数据数据流:DataInputStream、DataOutputStream,之前的数据流都是用来处于字节或字节数组的,是数据传输默认的数据类型。但是这两个数据流是用于读写Java基本数据类型

三、对象流

ObjectInputStream、ObjectOutputStream:用于将对象写入文件数据流或从文件数据流读出

try {
    Date d = new Date();
    FileOutputStream outputStream = new FileOutputStream("myfile");
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
    objectOutputStream.writeObject(d);
    objectOutputStream.flush();
    objectOutputStream.close();
} catch (IOException e) {
    e.printStackTrace();
}

四、序列化

把对象转换为字节序列的过程称为对象的序列化,把字节序列恢复为对象的过程称为对象的反序列化。

       序列化是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化,序列化是为了解决在对对象流进行读写操作时所引发的问题。java.io.Serializable接口中没有定义任何方法,只是作为一个标记来指示实现该接口的类可以进行序列化,而没有实现该接口的类的对象则不能长期保存其状态。当一个类实现了Serializable接口,表明该类加入了对象序列化协议。


public class Student implements Serializable {
    private String name;
    private int age;
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}
//对象的序列化
try {
    FileOutputStream outputStream = new FileOutputStream("/Users/wangchaojie/Downloads/1.txt");
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
    Student student = new Student("小李", 18);
    objectOutputStream.writeObject(student);
    objectOutputStream.flush();
    objectOutputStream.close();
} catch (IOException e) {
    e.printStackTrace();
}
//对象的反序列化
Student student;
try {
    FileInputStream inputStream = new FileInputStream("/Users/wangchaojie/Downloads/1.txt");
    ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
    student = (Student) objectInputStream.readObject();
    System.out.println(student.getName() + "" + student.getAge());
    objectInputStream.close();
} catch (IOException e) {
    e.printStackTrace();
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

并正确的反序列化出的对象数据:小李18

       序列化只能保存对象的非静态成员变量,而不能保存任何成员方法和静态成员变量(不能保存成员方法,自然也不能保存局部变量),并且保存的只是变量的值,对于变量的任何修饰符都无法保存。有一些对象不具有可持久性,是无法保存其状态的,必须用transient关键字修饰,任何被transient关键字修饰的成员变量都不会被保存。

public class Student implements Serializable {
    //name没有被序列化
    private transient String name;
    private int age;
}
反序列化后输出的是:null 18,name为null,没有被序列化上

5、基本字符流

Reader、Writer:专门用于处理字符流的类,也都是抽象类

      作用:Java通过Reader、Writer实现了对不同平台之间数据流的数据进行转换。其中最重要的子类InputStreamReader和OutputStreamWriter类,是字节流和读、写者之间的接口,用来在字节流和字符流之间作为中介

缓冲区字符流:BufferReader、BufferWriter

6、File类用于处理与文件相关的操作。File类可以得到文件的各种相关属性,然后将文件改名或删除,但是对于文件名以外的其他属性,File类不支持修改。File也可以描述目录,对其操作与文件相同,只是无法改变目录名,也不能进行删除。

7、RandomAccessFile类用于处理随机访问文件的操作,可以从文件的某个位置开始读取,到另一个位置读另一条记录,类似数据库的条件读取操作。

八、面向对象程序设计

顶级容器:JFrame、JApplet、JDialog、JWindow

属于容器的是:JPanel、JscrollPane等

文字的基本样式属性是:斜体

不包含本地代码的Swing组件被称为轻量级组件,包含本地代码的AWT组件被称为重量级组件

九、面向对象程序设计

组合框类名是JComboBox

具有构造方法JMenuBar()的类是菜单栏

某Java程序用javax.swing包中的类JFileChooser来实现打开和保存文件对话框,该程序通过文件对话框首先获得的信息是文件路径

十、多线程

一、进程和线程

       对于一般程序而言,在程序要投入运行时,系统从程序入口开始按语句的顺序(顺序、分支、循环结构)完成相应的指令至结尾,再从出口退出,整个程序结束。

一个进程既包含其所要执行的指令,又包括执行指令时所需要的任何系统资源,如CPU,内存空间,I/O端口等。

1.1、线程

       是进程执行过程中产生的多条执行线索,是比进程单位更小的执行单元。与进程相似的是:都是按序执行的语句,不同的是:它没有入口,也没有出口,因此其自身不能自动运行,必须栖身于一个进程中,由进程触发执行。在系统资源上,属于同一进程的所有线程共享该进程的系统资源,但是线程之间的切换速度比进程切换要快得多。

1.2、线程的结构

  • 虚拟CPU:封装在java.lang.Thread类中,它控制着整个线程的运行
  • 执行的代码:传递给Thread类,由Thread类控制按序进行
  • 处理的代码:传递给Thread类,是在代码执行过程中所要处理的数据

1.3、线程的状态

       Thread类知识线程的虚拟CPU,线程所执行的代码/线程要完成的功能,都是通过run()方法来实现的,方法run()称为方法体。在一个线程被创建并初始化之后,Java运行时系统自动调用run()方法,建立线程的目的得以实现。

  • 新建:线程对象刚创建,还没有启动,此时还处于不可运行的状态。但是相应的内存空间以及其他资源已经存在了。
  • 可运行:线程已经启动,处于线程的run()方法之中。该情况下,线程可能正在运行,也可能不在运行,只要CPU一空闲,就会马上运行。可以运行但是没运行的线程都放在一个就绪队列中,可运行并已运行的线程处于运行状态,等待运行的线程处于就绪状态。调用线程的start()方法可使线程处于可运行状态。start和run()之间的区别:好比开车的时候,start()是启动,但是不一定开走。
  • 死亡:线程死亡的两点原因:1、run()最后完整执行完毕;2、当线程遇到异常退出时
  • 阻塞:正在执行的线程因特殊原因,被暂停执行。阻塞的线程不进入就绪队列排队,必须等到阻塞的原因解决,才可重新进入队列排队。

线程一共只有上述四大种状态。

1.4、中断线程

常常调用interrupt()来终止进程。此方法不仅可以中断正在运行的线程,而且还能中断处于blocked状态的线程,并会抛出InterruptException异常。

测试线程是否被中断的几种方法

  • void interruopt():向一个线程发送一个中断请求,并把该线程的“interrupt”状态置为true。若该线程处于“blocked”状态,会抛出InterruptException异常。
  • static boolean interrupt():检测当前线程是否已被中断,并重置状态"interrupted"值。
  • boolean isInterrupted():检测当前线程是否已被中断,不改变状态"interrupted"值。

二、创建线程

继承Thread类:如果一个类继承Thread类,那么该类的对象就可以用来表示线程。

Thread(ThreadGroup group,Runnable target,String name);

此方法是Thread类的一个典型的构造方法,name是线程的名称,target必须实现Runnable接口,它是另一个线程对象,当本线程启动时,调用target对象的run();当targer对象为null时,调用本线程的run()。而在Runnable接口中,只定义了一个方法,即void run(),该方法为线程体。Thread类也实现了Runnable接口。

实现Runnable接口

从根本上讲,任何实现线程功能的类都必须实现该接口。用Runnable接口实现多线程时,必须实现run()方法,也需要使用start()启动线程。

static class TwoThread implements Runnable{
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            System.out.println("===");
        }
    }
}
public static void main(String[] args) {
    //静态类只能调用静态类,所以需要将TwoThread声明为静态的
    TwoThread twoThread = new TwoThread();
    Thread t1 = new Thread(twoThread);
    Thread t2 = new Thread(twoThread);
    //t1,t2都使用的是twoThread,它们共享twoThread,执行的都是twoThread的方法
    t1.start();
    t2.start();
}

三、线程的基本控制

3.1、线程的启动

虽然线程已经被创建,但是实际上并没有立刻运行。当其start()被调用时,线程才会被启动,虚拟CPU已经就绪。

  • start():启动线程对象,让线程由新建状态转为就绪状态
  • run():定义线程对象被调度后所执行的操作,必须重写run()
  • yield():强制终止线程的运行
  • isAlive():返回当前线程是否在活动
  • sleep(int millsecond):使线程休眠一段时间,单位毫秒
  • void wait():使线程处于等待状态

3.2、线程的调度

        就绪线程已经可以运行,并不代表一定立刻运行。Java中线程的调度是抢占式,并不是时间片式。可能多个线程准备运行,但是只有一个真正运行。当其中一个线程获得执行权时,该线程持续运行到其方法结束,或因为某种原因阻塞,或被另一个高优先级的就绪线程抢占。

3.3、线程优先级的策略

  • 根据优先级高低顺序执行
  • 每个线程被创建时都会被自动分配一个优先级,默认时,继承父类的优先级
  • 任务紧急的线程,优先级较高
  • 同一优先级的线程,遵循“先进先出”的调度原则
//Thread类中有3个与线程优先级相关的静态量
Thread.MIN_PRIORITY;//优先级最低,值为1
Thread.NORM_PRIORITY;//默认优先级,值为5
Thread.MAX_PRIORITY;//优先级最高,值为10

线程被阻塞的原因多种多样,可能是因为执行Thread.sleep(),故意让其暂停;也可能因为需要等待一个较慢的外部设备:磁盘或键盘等。所有被阻塞的线程按次序组成一个阻塞队列,所有就绪但是没有运行的线程则根据其优先级进入一个就绪队列,当CPU空闲时,就绪队列中第一个具有最高优先级的线程将运行。当一个线程被抢占而停止运行时,其运行状态就被改变并放到就绪队列的队尾,同样,一个被阻塞的线程就绪后通常也放到就绪队列的队尾。

可以通过调用sleep()或yield()合理安排线程的运行顺序。

  • Thread.sleep(x)直接调用,x代表当前线程必须休眠x毫秒后才有执行权,这个x只保证在一段时间后线程回到就绪状态,但是是否能被CPU立即调用,视情况而定,所以,一般暂停的时间比指定的时间要长。
  • yield()可以给其他同优先级的线程一个运行机会。如果在就绪队列中有其他同优先级的线程,那么yield()就把调用者放入到就绪队列队尾,并允许其他线程运行。如果没有这样的线程,其yield()不做任何工作。
  • 区别:sleep()允许低优先级的线程运行,yield()只给同优先级的线程运行机会。

3.4、结束线程

  • 线程自动执行结束并不可再被运行而消亡
  • 遇到异常使线程结束(强迫死亡)
  • 使用interrupt()中断线程的执行

3.5、挂起线程

暂停一个线程也称为挂起,在挂起之后,必须重新唤醒线程进入运行状态。

挂起线程的方式

  • sleep():用于暂停一个线程的运行。线程不是休眠期过后,立刻执行,因为此时可能还有其他线程正在执行,重新被执行的可能有:a.被唤醒的线程具有更高的优先级 b.正在执行的线程因为其他原因被阻塞 c.程序处于支持时间片的系统中
  • wait()和notify()/notifyAll():wait()导致当前线程等待,直到其他线程调用此线程的notify()/notifyAll()唤醒
  • join():将正在运行的线程等待,直到join()所调用的线程结束。比如线程B中调用的线程A的join(),直到线程A执行结束,才会执行线程B,可以理解成将线程A加入到当前线程B中

四、线程的互斥

当同时运行的线程需要共享数据时,那么每个线程就得考虑与它共享数据的其他线程的状态和行为,否则无法保证程序的安全性。

//栈模拟
class stack{
    int index = 0;
    char[] chars = new char[6];
    public void push(char c){
        chars[index]=c;
        index++;
    }
    public char pop(){
        index--;
        return chars[index];
    }
}

        上述的程序,如果有两个线程A,B,一个负责入栈操作,一个负责出栈操作。假设栈中有原始数据1和2,当线程A要入栈一个3,调用push(3),执行了语句chars[index]=3;后被其他线程抢占了,此时尚未执行index++,所以index的下标指向最后入栈的字符的下标,如果此时线程A马上被唤醒,可以继续修改index值得话,此无碍,操作完美。如果不能被唤醒的话,入栈操作执行了一半,恰巧线程B正在抢占了CPU,进行出栈操作,那么得到的数据就是2,因为先执行了index--,字符3被漏掉了。这就是多线程共享数据出现的问题。

五、对象的锁定标志

Java中引入了“对象互斥锁”的概念,阻止多个线程同时访问同一个条件变量

有两种方法可以实现“对象互斥锁”

  • 用关键字volatile来声明一个共享数据(变量)
  • 用关键字synchronized来声明操作共享数据的一个方法或一段代码

可以将对象想象成一间实验室,为众多实验人员共用,但任何时候实验室只允许一组实验人员在里面做实验,否则会引起混乱。为了控制,在门口加了一把锁,实验室没人的时候开放,有人进入实验室时第一件事就是将门锁上,然后开始工作,然后如果再有人希望进入,会因为门已被锁而只能等候,直到里面的实验人员完成工作后将锁打开才可进入。这种机制保证了当一个人员工作时不会被另一个人员打断,可以保证数据的完整性。同一时刻只能有一个任务被访问的代码区叫做临界区

一般来说,使用synchronized关键字在方法的层次上实现对共享资源操作的同步,很使用volatie关键字声明共享变量。

六、线程的同步

       为了完成多个任务,常常创建多个线程,可能它们之间毫无关系,但是有时候完成的任务有一些关系,所以需要线程之间有一些交互,在Java线程中,wait()、notify()、notifyAll()实现了线程的交互。

生产者和消费者问题

有两个人,一个人在刷盘子,另一个人在烘干。他们之间有一个共享对象-盘架,刷好而等待烘干的盘子放在盘架上。当盘架上有刷好的盘子时,烘干的人才能开始工作;而如果刷盘子的人刷的过快,刷好的盘子占满了盘架时,他就无法继续工作了,而需要等到盘架上有空位置才行。

说明了一个问题:生产者生产一个产品后就放入共享对象中,而不管共享对象中是否已有产品。消费者从共享对象中取用产品,但不检测是否已经取过。

可能会发生以下问题:

  • 生产者比消费者快时,消费者会漏掉一些数据没有取到
  • 消费者比生产者快时,消费者取到了相同的数据

解决方法

wait()方法导致当前的线程等待,它的作用是让当前线程释放其所持有的“对象互斥锁”,进入wait队列(等待队列);而notify()/notifyAll()方法的作用是唤醒一个或所有正在等待队列中等待的线程,并将它移入等待同一个“对象互斥锁”的队列。notify()、notifyAll()、wait()方法都只能在被声明为synchronized的方法或代码段中调用。notify()最多只能释放等待队列中的第一个线程,如果有多个线程在等待,则其他的线程将继续留在队列中,而notifyAll()方法能释放所有等待线程。

实际上,wait()方法既可以被notify()终止,又可以通过调用线程的interrupt()方法来终止。后一种情况下,wait()会抛出一个InterruptedException异常,所以需要放try/catch中。

sleep和wait的区别

  • 所属类:sleep来自Thread类,wait来自Object类。

  • sleep()可以在任何地方使用,wait()只能在同步方法和同步代码块中使用,因为需要获取对象的锁

  • sleep()一般用于当前线程休眠,或者轮循暂停操作,wait()则多用于多线程之间的通信

  • sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。

Thread.sleep(1000)的意思是:代码执行到这儿,1秒钟之内我停一下,就不参与CPU竞争了,1秒钟之后我再过来参与CPU竞争。

Thread.Sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芦蒿炒香干

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值