使用java关键字编写代码

使用java关键字编写代码

java的关键字

java的基本数据类型

Java是一种强类型语言,必须为每一个变量声明一种类型。Java共包含8中基本类型,其中4种整型、2种浮点型、1种用于表示Unicode编码的字符单元的字符类型char和1种用于表示真值的boolean类型。

Java包含4种整型:

  • int:4字节
  • short:2字节
  • long:8字节
  • byte:1字节

长整型数值有一个后缀L(如,999999999L)。十六进制有一个前缀0x(如,0xabc)。八进制有一个前缀0,例如011表示八进制中的9。

从Java 7开始,加上0b可以表示二进制数,例如0b101就是5。另外可以为数字字面量添加下划线(_)。如1000_000表示一百万。加下划线只是为了易读。

在Java中有两种浮点类型:

  • float:4字节
  • double:8字节

double表示的数值精度为float的两倍。在实际开发中,大部分情况下使用double而不使用float。

float类型有一个的数值后缀F或f或D(如,1.72F)。在没有后缀F的浮点数值(如,1.72)默认为double类型。

三个表示溢出和出错情况的三个特殊的浮点数:

  • 正无穷大 Double.POSITIVE_INFINITY
  • 负无穷大 Double.NEGATIVE_INFINITY
  • NaN(不是一个数),判断“非数值”,可以使用Double.isNaN方法

    char类型用于表示字符常量。例如:'A'是编码为65所对应的字符常量。而"A"表示一个包含字符A的字符串。Unicode编码单元可以表示为十六进制,其范围从\u0000到\uffff。除了采用转义序列符\u表示Unicode代码单元的编码之外,还有一些表示特殊字符的转义序列符,(如,\b退格,\n换行等)。

字符型常量有3种表示形式;

1,直接通过单个字符来指定字符型常量,如'A',‘B’,'5';

2,通过转义字符表示特殊字符型常量,如'\n','\\';

3,直接使用Unicode值来表示字符型常量,如'\u66f9','\u950b';

关于转义,有些字符属于特殊字符,不能直接使用,所以需要转义;

QQ鎴浘20160609111946.jpg

数值类型之间的转换

图中是实心箭头表示无信息丢失的转换;虚箭头表示可能有精度损失的转换。

java的包装类

每个基本类型都有一个对应的类;就是所谓的包装类;

QQ鎴浘20161020152251.jpg

1,装箱和拆箱

基本类型和类类型可以相互转换;

基本类型到类类型的转换叫做装箱;

类类型到基本类型的转换叫做拆箱;

    public static void main(String[] args) {
        int a=1;
        Integer i=new Integer(a); // 装箱
        int b=i.intValue(); // 拆箱
        System.out.println("a="+a);
        System.out.println("i="+i);
        System.out.println("b="+b);
    }
    public static void main(String[] args) {
        Integer i=1; // 自动装箱的过程 自动把基本类型转换成类类型
        int i2=i; // 自动拆箱的过程 自动把类类型转成基本类型
        System.out.println("i="+i);
        System.out.println("i2="+i2);
    }

 

java的访问控制权限

 

java的final关键字

  在程序中,一般用于将一个东西设置为不可改变。final关键字根据使用情景的不同会有一些细微的差异。有时候不清楚final关键字的用法,会造成误用。

      final关键字可以修饰变量、方法、类。

     一、使用final关键字修饰变量

     1、修饰基本数据类型

        在编写程序的时候,有时候需要告诉编译器某一个变量是一个“常数”,编译器常数,它永远不会改变,在运行期间初始化一个值,之后再也不会改变。定义一个常数的好处是:因为它永远不会改变,在编译的时候,编译器可以直接将它“放到”需要使用它的地方,在编译期间就可以使用它来进行运算,从而节省在运行时的开销。在Java中,这些形式的常数必须属于基本数据类型,而且需要使用final关键字修饰,使用final定义一个常数的时候,必须给出一个初始值。

      使用:final int a = 1;

     2、修饰对象

      final修饰对象的时候,是将这个对象名所指向的内存地址固定,这个对象(作用域范围内)永远被存储在这块内存空间里,而对象本身是可以改变的。如:

      class Person{

            public String name;

      }

     final Person p = new Person();

       这时候,因为使用final修饰了p这个对象变量,p指向一个地址空间,不能在给p赋值。即再次调用p= new Person();的时候会出现错误。而使用p.name = "";的时候是可以正常赋值的。

class Person {
    String name = "张三";
}

public class FinalDemo {
    public static void main(String[] args) {
        final Person p = new Person();
        p.name = "萧萧弈寒";
    }
}

      在一个类里面,使用final定义成员变量的时候,可以暂时不赋值,但是需要在类的构造函数中给这个成员变量赋值。如:

      class Person{

            final String name;

            public Person(){

                     name = "";

            }

     }

      这个时候相当于final修饰的变量延迟到构造函数中赋值。这是因为,这个变量使用的时候,构造函数已经被调用了,所以变量是一定赋值了的。

       注意:如果有多个构造函数,则每个构造函数中都需要给这个final修饰的变量赋值。 

     3、用final修饰参数

public class FinalDemo {

    static void f(final int i) {
        i++;    // 无法为final变量赋值,编译错误
    }

    public static void main(String[] args) {
        int x = 10;
        f(x);   // ①
    }
}

f方法只是将x的值赋给了i,实际上i和x是两个变量。

class Person {
    String name = "张三";
}

public class FinalDemo {

    public static void main(String[] args) {
        final Person p = new Person();
        changeName(p);
        System.out.println(p.name);
    }

    static void changeName(final Person p) {
        p.name = "萧萧弈寒";
    }
}

final并不能阻止changeName()方法改变p的内容,输出萧萧弈寒。

如果去掉changeName()修饰参数的final,然后在changeName方法体内改变p指向的实例:

    public static void main(String[] args) {
        final Person p = new Person();
        p.name = "萧萧弈寒";
        changeName(p);
        System.out.println(p.name);
    }

    static void changeName(Person p) {
        p = new Person();
    }

【运行结果】:
changeName中的name:张三
萧萧弈寒

我们可以看出,虽然方法体内的p指向了其他对象,但是对于main方法中的p并没有影响,原因还是Java是值传递的。

Java是值传递与引用传递,一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。Java总是采用按值调用。方法得到的是所有参数值的一个拷贝,特别的,方法不能修改传递给它的任何参数变量的内容。

基本数据类型为参数

public class ParamTest {
    public static void main(String[] args) {
        int price = 5;
        doubleValue(price);
        System.out.print(price);
    }

    public static void doubleValue(int x) {
        x = 2 * x;
    }
}
  1. doubleValue执行完后,参数变量不再使用,最后输出5,price的值并没有变化。

对象引用为参数

一个方法不能修改一个基本数据类型的参数。而对象引用作为参数就不同了。

public class ParamTest {
    public static void main(String[] args) {
        Student stu = new Student(80);
        raiseScore(stu);
        System.out.print(stu.getScore());
    }

    public static void raiseScore(Student s) {
        s.setScore(s.getScore() + 10);
    }
}

Student实例s的内容改变了,输出90.0。

Java对对象采用的不是引用调用,实际上,对象引用进行的是值传递。

java中方法参数的使用情况:

  • 一个方法不能修改一个基本数据类型的参数(即数值型和布尔型)
  • 一个方法可以改变一个对象参数的状态
  • 一个方法不能让对象参数引用一个新的对象

无论参数是基本数据类型,还是引用数据类型,只要加了final,不好意思,该参数不可以再赋值(实参传进来给形参,就相当于初始化完成)。。。。可以防止在方法里面不小心重新赋值。

不加final的参数,可能会出现的错误是

public static void printAll(Curry temp){
        temp = new Curry(); //这就是出现的问题,又new了一个库里,与调用该方法时传进来的库里毫无关系了!!
        temp.setSmallName("fk do it");
        System.out.println(temp.getSmallName());
 }

final修饰的变量是引用不能改变还是引用的对象不能改变


public class TestFinal {
	public static void main(String[] args) {
		final StringBuilder sb = new StringBuilder("haha");
		//同一对象的hashCode值相同
		System.out.println("sb中的内容是:"+sb);
		System.out.println(sb+"的哈希编码是:"+sb.hashCode());
		sb.append("我变了");
		System.out.println("sb中的内容是:"+sb);
		System.out.println(sb+"的哈希编码是:"+sb.hashCode());
		
 
		System.out.println("-----------------哈希值-------------------");
		TestFinal test = new TestFinal();
		System.out.println(test.hashCode());
		System.out.println(Integer.toHexString(test.hashCode()));
		System.out.println(test.getClass()+"@"+Integer.toHexString(test.hashCode()));
		System.out.println(test.getClass().getName()+"@"+Integer.toHexString(test.hashCode()));
		//在API中这么定义toString()等同于 getClass().getName() + '@' + Integer.toHexString(hashCode())
		//返回值是 a string representation of the object.
		System.out.println(test.toString());
	}

代码结果是:
sb中的内容是:haha
haha的哈希编码是:1928052572
sb中的内容是:haha我变了
haha我变了的哈希编码是:1928052572
-----------------哈希值-------------------
1398828021
53606bf5
class TestFinal@53606bf5
TestFinal@53606bf5
TestFinal@53606bf5

可以看出StringBuilder中的内容变了 而所指向的哈希编码却没发生变化,在Java中每一个对象都有自己独一无二的哈希编码,根据这个编码就可以找到相关的对象,也就是说,根据这个编码你可以独一无二地确定这个对象。
因而使用final关键字修饰一个变量时,是指引用变量不能变,引用变量所指向的对象中的内容还是可以改变的。

总得来说对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

     4、final方法

   final方法主要有两个方面的作用:一种是防止任何继承类覆盖方法。若希望一个方法的行为在继承期间保持不变,不可被覆盖和改写,就可以采取这种做法。另一种是提高程序执行的效率。将一个方法设成final后,编译器就会忽略为执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理)。

类内所有的private方法都自动成为final。由于不能访问一个private方法,所以它绝对不会被覆盖。

public class Parent {
    private void say(){
        System.out.println("parent");
    }
}

public class Son extends Parent {

    private void say(){
        System.out.println("son");
    }

}

 其实仔细看看,这种写法是方法的覆盖吗?我们通过多态的形式并不能调用到父类的 say() 方法:

  

  并且,如果我们在子类的 say() 方法中,添加 @Override 注解也是会报错的。

  

  所以这种形式并不算方法的覆盖。

     5、final类

    如果整个类都是final,就表明这个类不允许被继承。或者出于安全方面的理由,不希望进行子类化。除此之外,或许还考虑执行效率的问题,确保涉及这个类各对象的所有行动都要尽可能地有效。

由于禁止了继承,所以一个final类中的所有方法都默认为final。

 

java的对象初始化

构建器初始化

构建器进行初始化的优点是可以在运行期决定初始化值。

class Person {
    private int age;
    Person() {
        age = 89;
    }
}

age首先会初始化为0,然后变成89。对于所有基本类型以及对象的句柄,这种情况都是成立的。

java的内存对象模型

虚拟机栈

虚拟机栈是线程私有的,而且它的生命周期和线程相同.虚拟机栈是描述Java方法执行的内存模型。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口信息等,这里主要了解局部变量表部分。

局部变量表存放了编译时可知的各种基本数据类型和对象引用。需要注意的是long和double数据会占用2个局部变量空间,其它的都占一个。局部变量表的大小在编译时已经确定,所以在方法执行时不会改变局部变量表的大小。

虚拟机栈区域会出现StackOverflowError异常和OutOfMemoryError异常。

程序计数器

程序计数器可以看作是当前线程所执行的的字节码的指示器,只占用很小的内存空间。每个线程都需要有一个独立的程序计数器,各个线程之间的计数器互不影响,所以它也是线程隔离的数据区。

程序计数器是JVM中唯一一个没有规定OOM的区域。

本地方法栈

本地方法栈和虚拟机栈非常相似,它们的区别是虚拟机栈执行的是Java方法服务,而本地方法栈执行的是Native服务。

本地方法栈区域也会出现StackOverflowError异常和OutOfMemoryError异常。

Java堆

Java堆是被所有线程共享的,在虚拟机启动的时候创建,它的唯一目的就是存放对象实例。也就是说所有的对象实例和数组都要在堆上分配。

Java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。如果Java堆无法再继续扩展,而又有对象实例未完成分配,将会抛出OutOfMemoryError异常。

方法区

方法区也是各个线程共享的,主要用于存储已被虚拟机加载的类信息、常量、静态变量、编译后的代码等数据。

运行时常量池是方法区的一部分,主要用于存放编译生成的各种字面量和符号引用。

Java对象的创建

在Java中创建对象主要是通过new关键字,当虚拟机遇到new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用.并检查这个类是否已经被加载 解析和初始化,如果没有先执行类的加载过程。

经过上面的步骤,确定类已经被加载后,JVM就会为新生对象分配内存.对象所需的内存大小在类加载完成后就已经确定,所以只需要在Java堆中划分出确定大小的空间。内存的划分方式分为”指针碰撞”和”空闲列表”。

对象的访问

Java通过栈上的本地变量表的reference数据来操作Java堆上的对象。reference数据可以通过句柄或者指针的方式区访问对象。

通过句柄方式的话,Java堆中会划分出一块内存来存放句柄池,reference中存储的是句柄的地址,如图:

 

java的this和super关键字

this 关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。

this到底代表什么呢?this代表的是对象,具体代表哪个对象呢?哪个对象调用了this所在的方法,this就代表哪个对象。

this是一个引用,它指向自身的这个对象。

看内存分析图:

  假设我们在堆内存new了一个对象,在这个对象里面你想象着他有一个引用this,this指向这个对象自己,所以这就是this,这个new出来的对象名字是什么,我们不知道,不知道也没关系,因为这并不影响这个对象在内存里面的存在,这个对象只要在内存中存在,他就一定有一个引用this。

看下面的例子分析:

public class Leaf {
  
      int i = 0;
  
     public Leaf(int i) {
          this.i = i;
      }
 
     Leaf increament() {
         i++;
         return this;
     }
 
     void print() {
        System.out.println("i = " + i);
     }
 
     public static void main(String[] args) {
         Leaf leaf = new Leaf(100);
         leaf.increament().increament().print();
     }
 }

在内存中分析main方法的执行过程

首先分析第一句话:Leaf leaf = new Leaf(100);

  程序执行到这里的时候,栈空间里面有一个变量leaf,它指向了我们new出来的在堆空间里面的Leaf对象。new这个Leaf对象的时候,调用了构造方法Leaf(),这个构造方法里面有一个形参i,所以在栈空间里面给构造方法分配有一小块内存,名字叫i用来装传递过来的实参。这里传过来的实参是100,所以i里面装的值就是100。得到这个值之后,构造方法继续执行,执行this.i = i;这里就是把栈空间里面的i的值通过值传递给Leaf对象里面的成员变量i,所以成员变量i的值也变成了100。内存中的布局如下图所示:

  

  构造方法执行完之后,为这个构造方法分配的内存消失,所以栈里面的i所标记的那一小块内存会消失。因此第一句话执行完之后,内存中的布局如下图所示:

  

接下来分析第二句话:leaf.increament().increament().print();

  首先逐个分析:leaf.increament(),这里是调用increament()方法,是对new出来的那个Leaf对象调用的,leaf是Leaf对象的引用对象,因此通过这个引用对象来调用increament()方法,即相当于是Leaf对象自己调用了increament()方法。increament()方法的定义如下:

Leaf increament(){
  i++;
  return this;

}

 因此Leaf对象调用increament()方法时,首先执行方法体里面的第一句话i++;这样就把Leaf对象的成员变量i的值由原来的100变成了101。此时的内存布局如下图所示。

  接下来执行方法体里面的第二句话:return this;

  这里把this作为返回值,当有返回值的时候,首先会在栈里面给这个返回值分配一小块临时的存储空间。这块存储空间里面的内容是this里面的内容。this指向它自身,所以栈内存里面的那块临时存储空间里面装的this也是指向堆内存里面的Leaf对象。

所以leaf.increament().increament().print();这句话里面的left.increament()这一小句话执行完之后,内存中的布局如下图所示。

  

  leaf.increament().increament().print();这句话里面的left.increament()这一小句话执行完之后,返回一个this,此时leaf.increament().increament().print();就相当于是this.increament().print();

  接着栈里面的存储在临时空间里面的this调用increament()方法,而this指的就是Leaf对象,所以又是Leaf对象调用increament()方法。Leaf对象调用increament()方法时,又会执行方法体里面的i++,所以此时i又由原来的101变成了102。然后又执行return this,所以栈内存里面又多了一块临时存储空间,里面装的值也是this,这个this又是指向堆内存里面的Leaf对象。因此此时这个Leaf对象有了四个指向他自己的引用对象。

  leaf.increament().increament().print();这句话里面的leaf.increament().increament()这一小句话执行完之后,都返回了一个this,所以此时的leaf.increament().increament().print();就相当于是这样子的:this.this.print();

  接下来又是栈里面的那个新的this调用print()方法,使用this来调用,那就相当于是Leaf对象来调用,Leaf对象自己调用print()方法将自己的i属性的值打印出来,所以打印出来的结果应该是102。

  因此main方法里面的整个程序执行完之后,内存中的布局如下图所示:

  

  this的总结:this一般出现在方法里面,当这个方法还没有调用的时候,this指的是谁并不知道。但是实际当中,你如果new了一个对象出来,那么this指的就是当前这个对象。对哪个对象调用方法,this指的就是调用方法的这个对象(你对哪个对象调用这个方法,this指的就是谁)。如果再new一个对象,这个对象他也有自己的this,他自己的this就当然指的是他自己了。

this的应用

以Person为例,由于Person中的属性都被private了,外界无法直接访问属性,必须对外提供相应的set和get方法。

在开发中经常需要在创建对象的同时明确对象的属性值,比如员工入职公司就要明确他的姓名、年龄等属性信息。

没有显示指定构造方法,当在编译Java文件时,编译器会自动给class文件中添加默认的构造方法。如果在描述类时,我们显示指定了构造方法,那么,当在编译Java源文件时,编译器就不会再给class文件中添加默认构造方法。

构造方法在对象创建时就执行了,而且只执行一次。

一般方法是在对象创建后,需要使用时才被对象调用,并可以被多次调用。

有了构造方法之后可以对对象的属性进行初始化,那么还需要对应的set和get方法吗?

需要相应的set和get方法,因为对象在创建之后需要修改和访问相应的属性值时,在这时只能通过set或者get方法来操作。

调用其他构造方法,需要通过this关键字来调用

class Person {

private int age;

private String name;

Person() {

}

Person(String nm) {

name = nm;

}

Person(String nm, int a) {

this(nm);

age = a;

}

}

注意:通过 this 来调用构造方法,只能将这条代码放在构造函数的第一行,这是编译器的规定,如下所示:放在第二行会报错。

调用其他构造方法的语句必须定义在构造方法的第一行,原因是初始化动作要最先执行。

当在方法中出现了局部变量和成员变量同名的时候,那么在方法中怎么区别局部变量成员变量呢?可以在成员变量名前面加上this.来区别成员变量和局部变量。

使用this来调用本类中的属性

class Person {

private int age;

private String name;


// 给姓名和年龄初始化的构造方法

Person(String name, int age) {

// 当需要访问成员变量是,只需要在成员变量前面加上this.即可

this.name = name;

this.age = age;

}


public void speak() {

System.out.println("name=" + this.name + ",age=" + this.age);

}

}


class PersonDemo {

public static void main(String[] args) {

Person p = new Person("张三", 23);

p.speak();

}

}

this 表示当前对象,也就是调用该方法的对象,对象.name 肯定就是调用的成员变量。

this调用普通方法

this 表示当前对象,那么肯定能够调用当前类的普通方法。

     public void printName(){
         this.say();
     }
     
     public void say(){
         System.out.println("say method...");
     }

this返回当前对象

 public class ThisTest {
 
     public Object newObject(){
         return  this;
     }
 }

这表示的意思是谁调用 newObject() 方法,那么就返回谁的引用。

 

super关键字

 this 关键字是表示当前对象的引用。而 Java 中的 super 关键字则是表示 父类对象的引用。

内存分析

 分析任何程序都是从main方法的第一句开始分析的,所以首先分析main方法里面的第一句话:

ChlidClass cc = new ChlidClass();

  程序执行到这里时,首先在栈空间里面会产生一个变量cc,cc里面的值是什么这不好说,总而言之,通过这个值我们可以找到new出来的ChlidClass对象。由于子类ChlidClass是从父类FatherClass继承下来的,所以当我们new一个子类对象的时候,这个子类对象里面会包含有一个父类对象,而这个父类对象拥有他自身的属性value。这个value成员变量在FatherClass类里面声明的时候并没有对他进行初始化,所以系统默认给它初始化为0,成员变量(在类里面声明)在声明时可以不给它初始化,编译器会自动给这个成员变量初始化,但局部变量(在方法里面声明)在声明时一定要给它初始化,因为编译器不会自动给局部变量初始化,任何变量在使用之前必须对它进行初始化。

  子类在继承父类value属性的同时,自己也单独定义了一个value属性,所以当我们new出一个子类对象的时候,这个对象会有两个value属性,一个是从父类继承下来的value,另一个是自己的value。在子类里定义的成员变量value在声明时也没有给它初始化,所以编译器默认给它初始化为0。因此,执行完第一句话以后,系统内存的布局如下图所示:

 

接下来执行第二句话:

cc.f();

 当new一个对象出来的时候,这个对象会产生一个this的引用,这个this引用指向对象自身。如果new出来的对象是一个子类对象的话,那么这个子类对象里面还会有一个super引用,这个super指向当前对象里面的父对象。所以相当于程序里面有一个this,this指向对象自己,还有一个super,super指向当前对象里面的父对象。

  这里调用重写之后的f()方法,方法体内的第一句话:“super.f();”是让这个子类对象里面的父对象自己调用自己的f()方法去改变自己value属性的值,父对象通过指向他的引用super来调用自己的f()方法,所以执行完这一句以后,父对象里面的value的值变成了100。接着执行“value=200;”这里的vaule是子类对象自己声明的value,不是从父类继承下来的那个value。所以这句话执行完毕后,子类对象自己本身的value值变成了200。此时的内存布局如下图所示:

  方法体内的最后三句话都是执行打印value值的命令,前两句打印出来的是子类对象自己的那个value值,因此打印出来的结果为200,最后一句话打印的是这个子类对象里面的父类对象自己的value值,打印出来的结果为100。

  到此,整个内存分析就结束了,最终内存显示的结果如上面所示。

 

在JAVA类中使用super来引用父类的成分,用this来引用当前对象,如果一个类从另外一个类继承,我们new这个子类的实例对象的时候,这个子类对象里面会有一个父类对象。怎么去引用里面的父类对象呢?使用super来引用,this指的是当前对象的引用,super是当前对象里面的父对象的引用。

 /**
   * 父类
   * @author gacl
   *
   */
  class FatherClass {
      public int value;
     public void f() {
        value=100;
         System.out.println("父类的value属性值="+value);
     }
 }

 /**
  * 子类ChildClass从父类FatherClass继承
  * @author gacl
  *
  */
 class ChildClass extends FatherClass {
     /**
      * 子类除了继承父类所具有的valu属性外,自己又另外声明了一个value属性,
      * 也就是说,此时的子类拥有两个value属性。
      */
     public int value;
     /**
      * 在子类ChildClass里面重写了从父类继承下来的f()方法里面的实现,即重写了f()方法的方法体。
      */
     public void f() {
         super.f();//使用super作为父类对象的引用对象来调用父类对象里面的f()方法
         value=200;//这个value是子类自己定义的那个valu,不是从父类继承下来的那个value
         System.out.println("子类的value属性值="+value);
         System.out.println(value);//打印出来的是子类自定义的那个value的值,这个值是200
         /**
          * 打印出来的是父类里面的value值,由于子类在重写从父类继承下来的f()方法时,
          * 第一句话“super.f();”是让父类对象的引用对象调用父类对象的f()方法,
          * 即相当于是这个父类对象自己调用f()方法去改变自己的value属性的值,由0变了100。
          * 所以这里打印出来的value值是100。
          */
         System.out.println(super.value);
     }
 }
 
 /**
  * 测试类
  * @author gacl
  *
  */
 public class TestInherit {
     public static void main(String[] args) {
         ChildClass cc = new ChildClass();
         cc.f();
     }
 }

运行结果:

在创建子类对象的同时,也创建了父类的对象,而创建对象是通过调用构造函数实现的,那么我们在创建子类对象的时候,应该会调用父类的构造方法。

除了顶级类 Object.class 构造函数没有调用父类的构造方法,其余的所有类都默认在构造函数中调用了父类的构造函数(没有显式声明父类的子类其父类是 Object)。

子类默认是通过 super() 调用父类的无参构造方法,如果父类显示声明了一个有参构造方法,而没有声明无参构造方法,实例化子类是会报错的,解决办法就是通过 super 关键字调用父类的有参构造方法:

“父类对象的引用”,那说明我们使用的时候只能在子类中使用,既然是对象的引用,那么我们也可以用来调用成员属性以及成员方法,当然了,这里的 super 关键字还能够调用父类的构造方法。

在创建子类对象时,父类的构造方法会先执行,因为子类中所有构造方法的第一行有默认的隐式super();语句。

为什么子类对象创建都要访问父类中的构造方法?因为子类继承了父类的内容,所以创建对象时,必须要先看父类是如何对其内容进行初始化的,子类构造方法执行时中,调用了父类构造方法,这说明,子类构造方法中有一句super()。

原因:子类会继承父类中的内容,所以子类在初始化时,必须先到父类中去执行父类的初始化动作。这样,才可以使用父类中的内容。

当父类中没有空参数构造方法时,子类的构造方法必须有显示的super语句,指定要访问的父类有参数构造方法。

描述学生和工人这两个类,将他们的共性name和age抽取出来存放在父类中,并提供相应的get和set方法,同时需要在创建学生和工人对象就必须明确姓名和年龄。

class Person {

private String name;

private int age;

public Person(String name, int age) {

// super();

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;

}

}

class Student extends Person {

// Student类的构造方法

Student(String name, int age) {

// 使用super关键字调用父类构造方法,进行相应的初始化动作

super(name, age);

}

public void study() {// Studnet中特有的方法

System.out.println(this.getName() + "同学在学习");

}

}

class Worker extends Person {

Worker(String name, int age) {

// 使用super关键字调用父类构造方法,进行相应的初始化动作

super(name, age);

}

public void work() {// Worker 中特有的方法

System.out.println(this.getName() + "工人在工作");

// this关键字,本类对象的引用

// this是在方法中使用的,哪个对象调用了该方法,那么,this就代表调用该方法的对象引用

// this什么时候存在的?当创建对象的时候,this存在的
//System.out.println(super.getName() + "工人在工作");
}

}

public class Test {

public static void main(String[] args) {

Student stu = new Student("小明",23);

stu.study();

 

Worker w = new Worker("小李",45);

w.work();

}

}

继承中的构造方法注意事项:

1,如果我们手动给出了构造方法,编译器不会在给我们提供默认的空参数构造方法

   如果我们没写任何的构造方法,编译器提供给我们一个空参数构造方法

2, 在构造方法中,默认的第一条语句为 super();

   它是用来访问父类中的空参数构造方法,进行父类成员的初始化操作

3, 当父类中没有空参数构造方法的时候,怎么办?

a: 通过 super(参数) 访问父类有参数的构造方法

b: 通过 this(参数) 访问本类中其他构造方法

   注意:[本类中的其他构造方法已经能够正常访问父类构造方法]

4, super(参数) 与 this(参数) 不能同时在构造方法中存在,

      假设 super() 在 this() 关键字的前面

  首先通过 super() 调用父类构造方法,对父类进行一次实例化。接着调用 this() ,this() 方法会调用子类的构造方法,在子类的构造方法中又会对父类进行一次实例化。也就是说我们对子类进行一次实例化,对造成对父类进行两次实例化,所以显然编译器是不允许的。

  反过来 this() 在 super() 之前也是一样。

  而且编译器有限定 this() 和 super() 这两个关键字都只能出现在构造方法的第一行,将这两个关键字放在一起,总有一个关键字在第二行,编译是不能通过的。   

 

java的static关键字

首先,我们创建一个实体类 Person,有两个属性 name 和 age,都是普通成员变量(没有用 static 关键字修饰),接着我们通过其构造方法创建两个对象:

1 Person p1 = new Person("Tom",21);
2 Person p2 = new Person("Marry",20);
3 System.out.println(p1.toString());//Person{name='Tom', age=21}
4 System.out.println(p2.toString());//Person{name='Marry', age=20}

  这两个对象在内存中的存储结构如下:

  由上图可知,我们创建的两个对象 p1 和 p2  存储在堆中,但是其引用地址是存放在栈中的,而且这两个对象的两个变量互相独立,我们修改任何一个对象的属性值,是不改变另外一个对象的属性值的。

  下面我们将 Person 类中的 age 属性改为由 static 关键字修饰:

 我们发现第三行代码打印的 p1 对象 age 属性变为 20了,这是为什么呢?

  这是因为用在 jvm 的内存构造中,会在堆中开辟一块内存空间,专门用来存储用 static 修饰的成员变量,称为静态存储区,无论我们创建多少个对象,用 static 修饰的成员变量有且只有一份存储在静态存储区中,所以该静态变量的值是以最后创建对象时设置该静态变量的值为准,也就是由于 p1 先设置 age = 21,后来创建了 p2 对象,p2将 age 改为了20,那么该静态存储区的 age 属性值也被修改成了20。

  PS:在 JDK1.8 以前,静态存储区是存放在方法区的,而方法区不属于堆,在 JDK1.8 之后,才将方法区干掉了,方法区中的静态存储区改为到堆中存储。

  总结:static 修饰的变量被所有对象所共享,在内存中只有一个副本。由于与对象无关,所以我们可以直接通过 类名.静态变量 的方式来直接调用静态变量。对应的非静态变量是对象所拥有的,多少个对象就有多少个非静态变量,各个对象所拥有的副本不受影响。

静态成员变量与非静态成员变量的区别

  public class Cat {
  
      /**
       * 静态成员变量
       */
      private static int sid = 0;
 
    private String name;
 
     int id;
 
     Cat(String name) {
         this.name = name;
         id = sid++;
     }
 
     public void info() {
        System.out.println("My Name is " + name + ",NO." + id);
     }
 
     public static void main(String[] args) {
         Cat.sid = 100;
         Cat mimi = new Cat("mimi");
         Cat pipi = new Cat("pipi");
         mimi.info();
         pipi.info();
    }
 }

通过画内存分析图了解整个程序的执行过程

  执行程序的第一句话:Cat.sid = 100;时,这里的sid是一个静态成员变量,静态变量存放在数据区(data seg),所以首先在数据区里面分配一小块空间sid,第一句话执行完后,sid里面装着一个值就是100。

  此时的内存布局示意图如下所示

  

  接下来程序执行到:

    Cat  mimi = new Cat(“mimi”);

  这里,调用Cat类的构造方法Cat(String name),构造方法的定义如下:

    Cat ( String name){

      this.name = name;

      id=sid++;

    }

  调用时首先在栈内存里面分配一小块内存mm,里面装着可以找到在堆内存里面的Cat类的实例对象的地址,mm就是堆内存里面Cat类对象的引用对象。这个构造方法声明有字符串类型的形参变量,所以这里把“mimi”作为实参传递到构造方法里面,由于字符串常量是分配在数据区存储的,所以数据区里面多了一小块内存用来存储字符串“mimi”。此时的内存分布如下图所示:

  

  当调用构造方法时,首先在栈内存里面给形参name分配一小块空间,名字叫name,接下来把”mimi”这个字符串作为实参传递给name,字符串也是一种引用类型,除了那四类8种基础数据类型之外,其他所有的都是引用类型,所以可以认为字符串也是一个对象。所以这里相当于把”mimi”这个对象的引用传递给了name,所以现在name指向的是”mimi”。所以此时内存的布局如下图所示:

  接下来执行构造方法体里面的代码:

    this.name=name;

  这里的this指的是当前的对象,指的是堆内存里面的那只猫。这里把栈里面的name里面装着的值传递给堆内存里面的cat对象的name属性,所以此时这个name里面装着的值也是可以找到位于数据区里面的字符串对象“mimi”的,此时这个name也是字符串对象“mimi”的一个引用对象,通过它的属性值就可以找到位于数据区里面的字符串对象“mimi”。此时的内存分布如下图所示:

  

  接下来执行方法体内的另一句代码:

    id=sid++;

  这里是把sid的值传递给id,所以id的值是100,sid传递完以后,自己再加1,此时sid变成了101。此时的内存布局如下图所示。

  

  到此,构造方法调用完毕,给这个构造方法分配的局部变量所占的内存空间全部都要消失,所以位于栈空间里面的name这块内存消失了。栈内存里面指向数据区里面的字符串对象“mimi”的引用也消失了,此时只剩下堆内存里面的指向字符串对象“mimi”的引用没有消失。此时的内存布局如下图所示:

  

  接下来执行:Cat  pipi = new Cat(“pipi”);

  这里是第二次调用构造方法Cat(),整个调用过程与第一次一样,调用结束后,此时的内存布局如下图所示:

  

  最后两句代码是调用info()方法打印出来,打印结果如下:

  

  通过这个程序,看出来了这个静态成员变量sid的作用,它可以计数。每当有一只猫new出来的时候,就给它记一个数。让它自己往上加1。

  程序执行完后,内存中的整个布局就如上图所示了。一直持续到main方法调用完成的前一刻。

  这里调用构造方法Cat(String name) 创建出两只猫,首先在栈内存里面分配两小块空间mimi和pipi,里面分别装着可以找到这两只猫的地址,mimi和pipi对应着堆内存里面的两只猫的引用。这里的构造方法声明有字符串类型的变量,字符串常量是分配在数据区里面的,所以这里会把传过来的字符串mimi和pipi都存储到数据区里面。所以数据区里面分配有存储字符串mimi和pipi的两小块内存,里面装着字符串“mimi”和“pipi”,字符串也是引用类型,除了那四类8种的基础数据类型之外,其他所有的数据类型都是引用类型。所以可以认为字符串也是一个对象。

  这里是new了两只猫出来,这两只猫都有自己的id和name属性,所以这里的id和name都是非静态成员变量,即没有static修饰。所以每new出一只新猫,这只新猫都有属于它自己的id和name,即非静态成员变量id和name是每一个对象都有单独的一份。但对于静态成员变量来说,只有一份,不管new了多少个对象,哪怕不new对象,静态成员变量在数据区也会保留一份。如这里的sid一样,sid存放在数据区,无论new出来了多少只猫在堆内存里面,sid都只有一份,只在数据区保留一份。

  静态成员变量是属于整个类的,它不属于专门的某个对象。那么如何访问这个静态成员变量的值呢?首先第一点,任何一个对象都可以访问这个静态的值,访问的时候访问的都是同一块内存。第二点,即便是没有对象也可以访问这个静态的值,通过“类名.静态成员变量名”来访问这个静态的值,所以以后看到某一个类名加上“.”再加上后面有一个东西,那么后面这个东西一定是静态的,如”System.out”,这里就是通过类名(System类)再加上“.”来访问这个out的,所以这个out一定是静态的。

再看下面的这段代码

 public class Cat {
 
     /**
       * 这里面的sid不再是静态成员变量了,因为没有static修饰符,
      * 此时它就是类里面一个普通的非静态成员变量,和id,name一样,
       * 成为每一个new出来的对象都具有的属性。
       */
     private  int sid = 0;
 
     private String name;
 
     int id;

    Cat(String name) {
        this.name = name;
         id = sid++;
     }
 
     public void info() {
         System.out.println("My Name is " + name + ",NO." + id);
    }
 
     public static void main(String[] args) {
        //Cat.sid = 100;这里不能再使用“类.静态成员变量”的格式来访问sid了,因为sid现在变成了非静态的成员变量了。所以必须要把这句话注释掉,否则无法编译通过。
         Cat mimi = new Cat("mimi");
         Cat pipi = new Cat("pipi");
         mimi.info();
         pipi.info();
     }
 }

 这段代码与上一段代码唯一的区别是把声明sid变量的static修饰符给去掉了,此时的sid就不再是静态成员变量,而是非静态成员变量了,此时每一个new出来的cat对象都会有自己单独的sid属性。所以这段代码执行完成后,内存中的布局如下图所示:

  

  由于sid变成了非静态成员变量,所以不再有计数的功能了。sid与id和name属性一样,成为每一个new出来的对象都具有的属性,所以每一个new出来的cat都加上了一个sid属性。由于不能再使用”类名.静态成员对象名”的格式访问sid,所以代码的第一句”Cat.sid =100;”不能这样使用,否则编译会出错,必须把这句话注释掉才能编译成功。既然无法访问得到sid的值,所以sid的值就一直都是初始化时赋给的值0。直到调用构造方法时,执行到方法体内的代码id=sid++;时,sid首先把自身的值0赋值给id,所以id的值是0,然后sid自己加1,所以sid变成了1。

  所以静态变量和非静态变量的区别就在于静态变量可以用来计数,而非静态变量则不行。

  理解了内存,就理解了一切,就理解了各种各样的语言。所有的语言无非都是这样:局部变量分配内存永远在栈里面,new出来的东西分配内存永远是在堆里,静态的东西分配内存永远是在数据区。剩下的代码肯定是在代码区。所有的语言都是这样。

  在一个静态方法里,如果想访问一个非静态的成员变量,是不能直接访问的,必须在静态方法里new一个对象出来才能访问。如果是加了static的成员变量,那么这个成员变量就是一个静态的成员变量,就可以在main方法里面直接访问了。

  main方法是一个静态的方法,main方法要执行的时候不需要new一个对象出来。

  动态方法是针对于某一个对象调用的,静态方法不会针对某一个对象来调用,没有对象照样可以用。所以可以使用”classname.method()”.的形式来调用静态方法。所以想在main方法里面访问非静态成员变量是不可以的,想在main方法里面访问非静态方法也是不可以的,因为非静态方法只能针对于某个对象来调用,没有对象,就找不到方法的执行者了。

  成员变量只有在new出一个对象来的时候才在堆内存里面分配存储空间。局部变量在栈内存里面分配存储空间。

  静态方法不再是针对某一个对象来调用,所以不能访问非静态的成员。

  非静态成员专属于某一个对象,想访问非静态成员必须new一个对象出来才能访问。

  静态的变量可以通过对象名去访问,也可以通过类名去访问,两者访问的都是同一块内存。

static方法就是没有this的方法

在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。

简而言之,一句话来描述就是:方便在没有创建对象的情况下来进行调用(方法/变量)。

 随着类的加载而加载,所有优先于对象的存在

public class Student {
    public String name="王小明";
    public String country="中国";
    //打印name
    public void showName(){
        System.out.println(name);
    }
    public static void  showCountry(){
        System.out.println(country);//country报错,因为showCountry方法被static修饰,所以他随类的加载而加载,所以无发找到country。
    }
}

很显然,被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。

static可以用来修饰类的成员方法、类的成员变量,另外可以编写static代码块来优化程序性能。

     1、static方法

  static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,,因此只有通过类名来访问。对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。

  但是要注意的是,虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的。

  即使没有显示地使用static关键字,构造器实际上也是静态方法。实例构造器无法被隐藏或覆写,不参与多态,因而可以做静态绑定。从这个意义上可以认为实例构造器是“静态”的,但这种用法与Java语言定义的“静态方法”是两码事。 

  Java语言中,实例构造器只能在new表达式(或别的构造器)中被调用,不能通过方法调用表达式来调用。new表达式作为一个整体保证了对象的创建与初始化是打包在一起进行的,不能分开进行;但实例构造器只负责对象初始化的部分,“创建对象”的部分是由new表达式本身保证的。 
  1、Java的实例构造器只负责初始化,不负责创建对象;Java虚拟机的字节码指令的设计也反映了这一点,有一个new指令专门用于创建对象实例,而调用实例构造器则使用invokespecial指令。 
  2、“this”是作为实例构造器的第一个实际参数传入的。 

  静态方法中不能使用this,而构造器中可以使用this关键字。this是指调用当前方法的对象,而静态方法不属于任何对象。

     2、static变量

  static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

  static成员变量的初始化顺序按照定义的顺序进行初始化。static是不允许用来修饰局部变量。

  虽然对于静态方法来说没有this,那么在非静态方法中能够通过this访问静态成员变量吗?

public class Main {  
    static int value = 33;
 
    public static void main(String[] args) throws Exception{
        new Main().printValue();
    }
 
    private void printValue(){
        int value = 3;
        System.out.println(this.value);
    }
}

this代表当前对象,那么通过new Main()来调用printValue的话,当前对象就是通过new Main()生成的对象。而static变量是被对象所享有的,因此在printValue中的this.value的值毫无疑问是33。在printValue方法内部的value是局部变量,根本不可能与this关联,所以输出结果是33。在这里永远要记住一点:静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。

     3、static代码块

  static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。

public class Test extends Base{
 
    static{
        System.out.println("test static");
    }
     
    public Test(){
        System.out.println("test constructor");
    }
     
    public static void main(String[] args) {
        new Test();
    }
}
 
class Base{
     
    static{
        System.out.println("base static");
    }
     
    public Base(){
        System.out.println("base constructor");
    }
}

base static
test static
base constructor
test constructor

     4、静态导包

import static java.util.Arrays.*;

  public class StaticTest {
  
      public static void main(String[] args) {
         int[] arrays = {3,4,2,8,1,9};
         sort(arrays);
     }
 }

import static  java.util.Arrays.*,意思是导入 Arrays 类中的所有静态方法,当然你也可以将 * 变为某个方法名,也就是只导入该方法,那么我们在调用该方法时,就可以不带上类名,直接通过方法名来调用(第 11 行代码)。

  静态导包只会减少程序员的代码编写量,对于性能是没有任何提升的,反而会降低代码的可读性,所以实际如何使用需要权衡。

   5、静态内部类

内部类,定义在一个类的内部的类叫内部类,包含内部类的类叫外部类,内部类用 static 修饰便是我们所说的静态内部类。

定义内部类的好处是外部类可以访问内部类的所有方法和属性,包括私有方法和私有属性。

访问普通内部类,我们需要先创建外部类的对象,然后通过外部类名.new 创建内部类的实例。

  public class OutClass {
  
      public class InnerClass{
  
     }
 }
 OuterClass oc = new OuterClass();
 OuterClass.InnerClass in = oc.new InnerClass();

 访问静态内部类,我们不需要创建外部类的对象,可以直接通过 外部类名.内部类名 来创建实例。

  public class OutClass {
  
      public static class InnerClass{
  
     }
 }
OuterClass.StaticInnerClass sic = new OuterClass.StaticInnerClass();

 

父类和子类执行顺序

对象的初始化顺序:

  首先执行父类静态的内容,父类静态的内容执行完毕后,接着去执行子类的静态的内容,当子类的静态内容执行完毕之后,再去看父类有没有构造代码块,如果有就执行父类的构造代码块,父类的构造代码块执行完毕,接着执行父类的构造方法;父类的构造方法执行完毕之后,它接着去看子类有没有构造代码块,如果有就执行子类的构造代码块。子类的构造代码块执行完毕再去执行子类的构造方法。

  总之一句话,静态代码块内容先执行,接着执行父类构造代码块和构造方法,然后执行子类构造代码块和构造方法。

     代码具体的执行过程,在执行开始,先要寻找到main方法,因为main方法是程序的入口,但是在执行main方法之前,必须先加载Test类,而在加载Test类的时候发现Test类继承自Base类,因此会转去先加载Base类,在加载Base类的时候,发现有static块,便执行了static块。在Base类加载完成之后,便继续加载Test类,然后发现Test类中也有static块,便执行static块。在加载完所需的类之后,便开始执行main方法。在main方法中执行new Test()的时候会先调用父类的构造器,然后再调用自身的构造器。因此,便出现了上面的输出结果。

public class Test {
    Person person = new Person("Test");
    static{
        System.out.println("test static");
    }
     
    public Test() {
        System.out.println("test constructor");
    }
     
    public static void main(String[] args) {
        new MyClass();
    }
}
 
class Person{
    static{
        System.out.println("person static");
    }
    public Person(String str) {
        System.out.println("person "+str);
    }
}
 
 
class MyClass extends Test {
    Person person = new Person("MyClass");
    static{
        System.out.println("myclass static");
    }
     
    public MyClass() {
        System.out.println("myclass constructor");
    }
}

test static
myclass static
person static
person Test
test constructor
person MyClass
myclass constructor

  类似地,我们还是来想一下这段代码的具体执行过程。首先加载Test类,因此会执行Test类中的static块。接着执行new MyClass(),而MyClass类还没有被加载,因此需要加载MyClass类。在加载MyClass类的时候,发现MyClass类继承自Test类,但是由于Test类已经被加载了,所以只需要加载MyClass类,那么就会执行MyClass类的中的static块。在加载完之后,就通过构造器来生成对象。而在生成对象的时候,必须先初始化父类的成员变量,因此会执行Test中的Person person = new Person(),而Person类还没有被加载过,因此会先加载Person类并执行Person类中的static块,接着执行父类的构造器,完成了父类的初始化,然后就来初始化自身了,因此会接着执行MyClass中的Person person = new Person(),最后执行MyClass的构造器。

  为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。

       isBornBoomer是用来这个人是否是1946-1964年出生的,而每次isBornBoomer被调用的时候,都会生成startDate和birthDate两个对象,造成了空间浪费,如果改成这样效率会更好:

class Person{
    private Date birthDate;
    private static Date startDate,endDate;
    static{
        startDate = Date.valueOf("1946");
        endDate = Date.valueOf("1964");
    }
     
    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }
     
    boolean isBornBoomer() {
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

静态代码块不能存在任何方法体中

  这个应该很好理解,首先我们要明确静态代码块是在类加载的时候就要运行了。我们分情况讨论:

  对于普通方法,由于普通方法是通过加载类,然后new出实例化对象,通过对象才能运行这个方法,而静态代码块只需要加载类之后就能运行了。

  对于静态方法,在类加载的时候,静态方法也已经加载了,但是我们必须要通过类名或者对象名才能访问,也就是说相比于静态代码块,静态代码块是主动运行的,而静态方法是被动运行的。

  不管是哪种方法,我们需要明确静态代码块的存在在类加载的时候就自动运行了,而放在不管是普通方法还是静态方法中,都是不能自动运行的。

静态代码块不能访问普通变量

  这个理解思维同上,普通变量只能通过对象来调用,是不能放在静态代码块中的。

 

静态代码块>构造代码块>构造函数>普通代码块 

构造代码块

构造代码块在创建对象时被调用,每次创建对象都会调用一次,但是优先于构造函数执行。需要注意的是,听名字我们就知道,构造代码块不是优先于构造函数执行,而是依托于构造函数,也就是说,如果你不实例化对象,构造代码块是不会执行的。

和构造函数的作用类似,都能对对象进行初始化,并且只要创建一个对象,构造代码块都会执行一次。但是反过来,构造函数则不一定每个对象建立时都执行(多个构造函数情况下,建立对象时传入的参数不同则初始化使用对应的构造函数)。

  利用每次创建对象的时候都会提前调用一次构造代码块特性,我们可以做诸如统计创建对象的次数等功能。

public class CodeBlock {
    {
        System.out.println("构造代码块");
    }
     
    public CodeBlock(){
        System.out.println("无参构造函数");
    }
    public CodeBlock(String str){
        System.out.println("有参构造函数");
    }
}

我们反编译生成的class文件:

 

普通代码块

普通代码块和构造代码块的区别是,构造代码块是在类中定义的,而普通代码块是在方法体中定义的。且普通代码块的执行顺序和书写顺序一致。

public void sayHello(){
    {
        System.out.println("普通代码块");
    }
}

其他常见static问题整理

    ①、静态变量能存在于普通方法中吗?

  能。很明显,普通方法必须通过对象来调用,静态变量都可以直接通过类名来调用了,更不用说通过对象来调用,所以是可以存在于普通方法中的。

  ②、静态方法能存在普通变量吗?

  不能。因为静态方法可以直接通过类名来直接调用,不用创建对象,而普通变量是必须通过对象来调用的。那么将普通变量放在静态方法中,在直接通过类来调用静态方法时就会报错。所以不能。

  ③、静态代码块能放在方法体中吗?

  不能。首先我们要明确静态代码块是在类加载的时候自动运行的。

  普通方法需要我们创建对象,然后手工去调用方法,所静态代码块不能声明在普通方法中。

  那么对于用 static 修饰的静态方法呢?同样也是不能的。因为静态方法同样也需要我们手工通过类名来调用,而不是直接在类加载的时候就运行了。

  也就是说静态代码块能够自动执行,而不管是普通方法还是静态方法都是需要手工执行的。

  ④、静态导包会比普通导包消耗更多的性能?

  不会。静态导包实际上在编译期间都会被编译器进行处理,将其转换成普通按需导包的形式,所以在程序运行期间是不影响性能的。

  ⑤、static 可以用来修饰局部变量吗?

  不能。不管是在普通方法还是在静态方法中,static 关键字都不能用来修饰局部变量,这是Java的规定。稍微想想也能明白,局部变量的声明周期是随着方法的结束而结束的,因为static 修饰的变量是全局的,不与对象有关的,如果用 static 修饰局部变量容易造成理解上的冲突,所以Java规定 static 关键字不能用来修饰局部变量。

 

java的abstract和extends以及interfac和implements关键字

extends关键字

继承的作用在于代码的复用。由于继承意味着父类的所有方法亦可在子类中使用,所以发给父类的消息亦可发给衍生类。如果Person类中有一个eat方法,那么Student类中也会有这个方法,这意味着Student对象也是Person的一种类型。

class Person {

    public void eat(Value v) {
        System.out.println("Person.eat()");
    }
}

class Teacher extends Person {
    public void eat(Value v) {
        System.out.println("Teacher.eat()");
    }
}

class Student extends Person {
    public void eat(Value v) {
        System.out.println("Student.eat()");
    }
}

public class UpcastingDemo {
    public static void show(Student s) {
        s.eat(Value.v1);
    }

    public static void show(Teacher t) {
        t.eat(Value.v1);
    }

    public static void show(Person p) {
        p.eat(Value.v1);
    }

    public static void main(String[] args) {
        Student s = new Student();
        Teacher t = new Teacher();
        Person p = new Person();
        show(s);
        show(t);
        show(p);
    }
}

这种做法一个很明显的缺陷就是必须为每一个Person类的衍生类定义与之紧密相关的方法,产生了很多重复的代码。另一方面,对于如果忘记了方法的重载也不会报错。上例中的三个show方法完全可以合并为一个:

public static void show(Person p) {
     p.eat(Value.v1);
}

将一个方法同一个方法主体连接在一起就称为绑定(Binding)。若在运行运行前执行绑定,就称为“早期绑定”。上面的例子中,在只有一个Person句柄的情况下,编译器不知道具体调用哪个方法。Java实现了一种方法调用机制,可在运行期间判断对象的类型,然后调用相应的方法,这种在运行期间进行,以对象的类型为基础的绑定称为动态绑定。除非一个方法被声明为final,Java中的所有方法都是动态绑定的。

动态绑定是有前提的,绑定的方法必须存在于基类中,否则无法编译通过。

class Person {
    void eat() {
        System.out.println("Person.eat()");
    }
}
class Boy extends Person {
    void eat() {
        System.out.println("Boy.eat()");
    }
    void speak() {
        System.out.println("Boy.speak()");
    }
}
public class Persons {
    public static void main(String[] args) {
        Person p = new Boy();
        p.eat();
        p.speak();  // The method speak() is undefined for the type Person
    }
}

对于静态方法而言,不管父类引用指向的什么子类对象,调用的都是父类的方法。

- 静态方法:静态方法看父类 
- 非静态方法:非静态方法看子类

abstract关键字

 抽象方法必须用abstract关键字进行修饰。如果一个类含有抽象方法,则称这个类为抽象类,抽象类必须在类前用abstract关键字修饰。因为抽象类中含有无具体实现的方法,所以不能用抽象类创建对象。

  从这里可以看出,抽象类就是为了继承而存在的,如果你定义了一个抽象类,却不去继承它,那么等于白白创建了这个抽象类,因为你不能用它来做任何事情。对于一个父类,如果它的某个方法在父类中实现出来没有任何意义,必须根据子类的实际需求来进行不同的实现,那么就可以将这个方法声明为abstract方法,此时这个类也就成为abstract类了。

  包含抽象方法的类称为抽象类,但并不意味着抽象类中只能有抽象方法,它和普通类一样,同样可以拥有成员变量和普通的成员方法。注意,抽象类和普通类的主要有三点区别:

  1)抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public。

  2)抽象类不能用来创建对象;

  3)如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类。

  在其他方面,抽象类和普通的类并没有区别。

模板方法模式是基于继承的代码复用技术的。在模板方法模式中,我们可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中。也就是说我们需要声明一个抽象的父类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法让子类来实现剩余的逻辑,不同的子类可以以不同的方式来实现这些逻辑。

其实所谓模板就是一个方法,这个方法将算法的实现定义成了一组步骤,其中任何步骤都是可以抽象的,交由子类来负责实现。这样就可以保证算法的结构保持不变,同时由子类提供部分实现。

模板是一个方法,那么他与普通的方法存在什么不同呢?模板方法是定义在抽象类中,把基本操作方法组合在一起形成一个总算法或者一组步骤的方法。而普通的方法是实现各个步骤的方法,我们可以认为普通方法是模板方法的一个组成部分。

模式结构

从上面的结构可以看出,模板方法模式就两个角色:

AbstractClass: 抽象类

ConcreteClass:  具体子类

其中抽象类提供一组算法和部分逻辑的实现,具体子类实现剩余逻辑。

模板方法场景

具有统一的操作步骤或操作过程

存在多个具有同样操作步骤的应用场景,其中只有细节不一样。

组装不同的车的步骤都是一样的

public abstract class MakeCar {
    public abstract void makeHead();
    public abstract void makeBody();
    public abstract void makeTail();
}

模板方法模式:由于组装车的三个步骤是所有车共有的,我们在抽象类中抽象出来

public abstract class MakeCar {
    public abstract void makeHead();
    public abstract void makeBody();
    public abstract void makeTail();
 
    public void make(){
        this.makeHead();
        this.makeBody();
        this.makeTail();
    }
}

新加组装jeep

public class MakeJeep extends MakeCar{
    @Override
    public void makeHead() {
        System.out.println("组装Jeep 车头");
    }
 
    @Override
    public void makeBody() {
        System.out.println("组装Jeep 车身");
    }
 
    @Override
    public void makeTail() {
        System.out.println("组装Jeep 车尾");
    }
}
   public static void main(String[] args) {
        MakeCar bus = new MakeBus();
        bus.make();
 
        MakeCar jeep = new MakeJeep();
        jeep.make();
    }

优点

1、模板方法模式在定义了一组算法,将具体的实现交由子类负责。

2、模板方法模式是一种代码复用的基本技术。

3、模板方法模式导致一种反向的控制结构,通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,符合“开闭原则”。

缺点

每一个不同的实现都需要一个子类来实现,导致类的个数增加,是的系统更加庞大。

使用场景

1、 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。

2、 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。

3、控制子类的扩展。

模式总结

1、 模板方法模式定义了算法的步骤,将这些步骤的实现延迟到了子类。

2、 模板方法模式为我们提供了一种代码复用的重要技巧。

3、 模板方法模式的抽象类可以定义抽象方法、具体方法和钩子。

4、 为了防止子类改变算法的实现步骤,我们可以将模板方法声明为final。

l 根据员工信息的描述,确定每个员工都有员工编号、姓名、要进行工作。则,把这些共同的属性与功能抽取到父类中(员工类),关于工作的内容由具体的工程师来进行指定。

定义员工类(抽象类)

public abstract class Employee {

private String id;// 员工编号

private String name; // 员工姓名

 

//空参数构造方法

public Employee() {

super();

}

//有参数构造方法

public Employee(String id, String name) {

super();

this.id = id;

this.name = name;

}
//工作方法(抽象方法)

public abstract void work();

}
}

定义研发部员工类Developer 继承 员工类Employee

public abstract class Developer extends Employee {

//空参数构造方法

public Developer() {

super();

}

//有参数构造方法

public Developer(String id, String name) {

super(id, name);

}

}

定义JavaEE工程师 继承 研发部员工类,重写工作方法

public class JavaEE extends Developer {

//空参数构造方法

public JavaEE() {

super();

}

//有参数构造方法

public JavaEE(String id, String name) {

super(id, name);

}

 

@Override

public void work() {

System.out.println("员工号为 " + getId() + " 的 " + getName() + " 员工,正在研发淘宝网站");

}

}

定义Android工程师 继承 研发部员工类,重写工作方法

public class Android extends Developer {

//空参数构造方法

public Android() {

super();

}

//有参数构造方法

public Android(String id, String name) {

super(id, name);

}

 

@Override

public void work() {

System.out.println("员工号为 " + getId() + " 的 " + getName() + " 员工,正在研发淘宝手机客户端软件");

}

}

interface关键字

  接口,英文称作interface,在软件工程中,接口泛指供别人调用的方法或者函数。从这里,我们可以体会到Java语言设计者的初衷,它是对行为的抽象。

  接口中可以含有 变量和方法。但是要注意,接口中的变量会被隐式地指定为public static final变量(并且只能是public static final变量,用private修饰会报编译错误),而方法会被隐式地指定为public abstract方法且只能是public abstract方法(用其他关键字,比如private、protected、static、 final等修饰会报编译错误),并且接口中所有的方法不能有具体的实现,也就是说,接口中的方法必须都是抽象方法。从这里可以隐约看出接口和抽象类的区别,接口是一种极度抽象的类型,它比抽象类更加“抽象”,并且一般情况下不在接口中定义变量。

抽象类和接口的区别

1.语法层面上的区别

  1)抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;

  2)抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;

  3)接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;

  4)一个类只能继承一个抽象类,而一个类却可以实现多个接口。

2.设计层面上的区别

  1)抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口Fly,包含方法fly( ),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。从这里可以看出,继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。

  2)设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。什么是模板式设计?最简单例子,大家都用过ppt里面的模板,如果用模板A设计了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它们的公共部分需要改动,则只需要改动模板A就可以了,不需要重新对ppt B和ppt C进行改动。而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。

  下面看一个网上流传最广泛的例子:门和警报的例子:门都有open( )和close( )两个动作,此时我们可以定义通过抽象类和接口来定义这个抽象概念:

abstract class Door {
    public abstract void open();
    public abstract void close();
}

 或者:

interface Door {
    public abstract void open();
    public abstract void close();
}

  但是现在如果我们需要门具有报警alarm( )的功能,那么该如何实现?下面提供两种思路:

  1)将这三个功能都放在抽象类里面,但是这样一来所有继承于这个抽象类的子类都具备了报警功能,但是有的门并不一定具备报警功能;

  2)将这三个功能都放在接口里面,需要用到报警功能的类就需要实现这个接口中的open( )和close( ),也许这个类根本就不具备open( )和close( )这两个功能,比如火灾报警器。

  从这里可以看出, Door的open() 、close()和alarm()根本就属于两个不同范畴内的行为,open()和close()属于门本身固有的行为特性,而alarm()属于延伸的附加行为。因此最好的解决办法是单独将报警设计为一个接口,包含alarm()行为,Door设计为单独的一个抽象类,包含open和close两种行为。再设计一个报警门继承Door类和实现Alarm接口。

interface Alram {
    void alarm();
}
 
abstract class Door {
    void open();
    void close();
}
 
class AlarmDoor extends Door implements Alarm {
    void oepn() {
      //....
    }
    void close() {
      //....
    }
    void alarm() {
      //....
    }
}

Java有一个特点就是单继承、多实现

public class B {
    private String name;
    public void setName(String name) {
        this.name = name;
    }
   public String getName() {
        return name;
    }
}
public interface C{
     String name="二狗子";
     void setName();
     void setName(int i);
}
public interface D {
    void setName();
    void setName(String name);
}
public interface E extends C,D{
    void setName(long l);
}

以上,我们定义了3个接口,其中E继承于C、D,这已经说明在接口上完全是可以多继承的!

public class A extends B implements C,D {
    @Override
    public void setName() {
    }

    @Override
    public void setName(int i) {
    }
}

类A继承了类B,而类B中有setName(String name)这个方法,接口D中的setName(String name)抽象方法由A的父类B代为实现了。

对象的多态性

Java中的多态性表现:

1,方法的重载和重写;

2,可以用父类的引用指向子类的具体实现,而且可以随时更换为其他子类的具体实现;

public class Animal {
 
    public void say(){
        System.out.println("我是一个动物");
    }
}

两个子类,分别是Dog和Cat类,重写父类方法say:

public class Cat extends Animal{
 
    public void say(){
        System.out.println("我是一个猫");
    }
}
public class Cat extends Animal{
 
    public void say(){
        System.out.println("我是一个猫");
    }
}

父类引用指向子类具体实现:

public class Test {
 
    public static void main(String[] args) {
        // 父类引用指向Dog子类的具体实现
        Animal animal=new Dog();
        animal.say();
         
        // 更换实现
        animal=new Cat();
        animal.say();
    }
}

对象的转型:

向上转型:子类对象->父类对象 安全

向下转型:父类对象->子类对象 不安全

比如上面的实例:Animal animal=new Dog(); 就是后面的new Dog() 子类对象 向上  Animail animal 转型  是安全的;

向下转型 是父类对象强制转换成子类对象,会报转换异常 所以说向下转型是不安全的,我们必须知道具体的实现类;

    public static void main(String[] args) {
        // 父类引用指向Dog子类的具体实现
        Animal animal=new Dog();
        animal.say();
         
        // 向下转型
        Dog dog=(Dog) animal;
        dog.say();
         
        // 向下转型  因为animal指向的是Dog具体实现 所以会报错
        Cat cat=(Cat)animal;
        cat.say();
    }

实际开发中更多的是用接口

public interface People {
 
    public void say();
}

public class Student implements People{
 
    @Override
    public void say() {
        System.out.println("我是学生");
    }
 
}

public class Teacher implements People{
 
    @Override
    public void say() {
        System.out.println("我是老师");
    }
 
}

  public static void main(String[] args) {
        People p1=new Student();
        p1.say();
         
        p1=new Teacher();
        p1.say();
    }

 

java的try和throws与finally关键字

Java异常类层次结构图

在 Java 中,所有的异常都有一个共同的祖先 Throwable(可抛出)。Throwable 指定代码中可用异常传播机制通过 Java 应用程序传输的任何问题的共性。

Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
Exception(异常):是程序本身可以处理的异常。Exception是检查型异常,在程序中必须使用try...catch进行处理;
Exception 类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。
注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
通常,Java的异常(包括Exception和Error)分为可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)。
可查异常(编译器要求必须处置的异常):正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

不可查异常(编译器不要求强制处置的异常):包括运行时异常(RuntimeException与其子类)和错误(Error)。

Exception 这种异常分两大类运行时异常和非运行时异常(编译异常)。程序中应当尽可能去处理这些异常。

运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。RuntimeException最好也用try...catch捕获;

运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。

非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

public class Demo1 {
 
    /**
     * 运行时异常,编译时不检查,可以不使用try...catch捕获
     * @throws RuntimeException
     */
    public static void testRuntimeException()throws RuntimeException{
        throw new RuntimeException("运行时异常");
    }
     
    /**
     * Exception异常,编译时会检查,必须使用try...catch捕获
     * @throws Exception
     */
    public static void testException()throws Exception{
        throw new Exception("Exception异常");
    }
     
    public static void main(String[] args) {
        try {
            testException();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
         
        testRuntimeException();
    }
}

处理异常机制

在 Java 应用程序中,异常处理机制为:抛出异常,捕捉异常。

抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。

throws表示当前方法不处理异常,而是交给方法的调用处去处理;

throw表示直接抛出一个异常;

    /**
     * 把异常向外面抛
     * @throws NumberFormatException
     */
    public static void testThrows()throws NumberFormatException{
        String str="123a";
        int a=Integer.parseInt(str);
        System.out.println(a);
    }

throw表示直接抛出一个异常;我们可以根据业务在代码任何地方抛出异常:

public class Demo {
 
    public static void testThrow(int a) throws Exception{
        if(a==1){
            // 直接抛出一个异常类
            throw new Exception("有异常");
        }
        System.out.println(a);
    }
     
    public static void main(String[] args) {
        try {
            testThrow(1);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

捕获异常 :在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适 的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适 的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

java程序中的异常我们可以捕获然后处理,这样后面的程序就可以继续执行了;

在捕获异常的时候,假如我们不确定会抛出什么异常,我们可以写多个异常捕获:

    public static void main(String[] args) {
        String str="123a";
        try{
            int a=Integer.parseInt(str);          
        }catch(NullPointerException e){
            e.printStackTrace();
        }catch(NumberFormatException e){
            e.printStackTrace();
        }catch(Exception e){
            e.printStackTrace();
        }
        System.out.println("继续执行");
    }

注意 由上往下的异常 必须范围同级别或者更高;否则编译报错;

try...cacth...finally 假如我们有种需求,不管有没有发生异常,比如执行某些代码,这时候,finally就派上用场了;

对于运行时异常、错误或可查异常,Java技术所要求的异常处理方式有所不同。

由于运行时异常的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常。

对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。

对于所有的可查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉可查异常时,它必须声明将抛出异常。

能够捕捉异常的方法,需要提供相符类型的异常处理器。所捕捉的异常,可能是由于自身语句所引发并抛出的异常,也可能是由某个调用的方法或者Java运行时 系统等抛出的异常。也就是说,一个方法所能捕捉的异常,一定是Java代码在某处所抛出的异常。简单地说,异常总是先被抛出,后被捕捉的。

任何Java代码都可以抛出异常,如:自己编写的代码、来自Java开发环境包中代码,或者Java运行时系统。无论是谁,都可以通过Java的throw语句抛出异常。

从方法中抛出的任何异常都必须使用throws子句。

捕捉异常通过try-catch语句或者try-catch-finally语句实现。

总体来说,Java规定:对于可查异常必须捕捉、或者声明抛出。允许忽略不可查的RuntimeException和Error。

自定义异常

自定义异常要继承自Exception;

/**
 * 自定义异常,继承自Exception
 * @author user
 *
 */
public class CustomException extends Exception{
 
    public CustomException(String message) {
        super(message);
    }
 
}


public class TestCustomException {
 
    public static void test()throws CustomException{
        throw new CustomException("自定义异常");
    }
     
    public static void main(String[] args) {
        try {
            test();
        } catch (CustomException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

 

java的instanceof关键字

boolean result = obj instanceof Class,obj 必须为引用类型,不能是基本类型。

System.out.println(null instanceof Object);//false

一般我们知道Java分为两种数据类型,一种是基本数据类型,有八个分别是 byte  short  int  long  float  double  char boolean,一种是引用类型,包括类,接口,数组等等。而Java中还有一种特殊的 null 类型,该类型没有名字,所以不可能声明为 null 类型的变量或者转换为 null 类型,null 引用是 null 类型表达式唯一可能的值,null 引用也可以转换为任意引用类型。我们不需要对 null 类型有多深刻的了解,我们只需要知道 null 是可以成为任意引用类型的特殊符号

编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译

Person p1 = new Person();
 
System.out.println(p1 instanceof String);//编译报错
System.out.println(p1 instanceof List);//false
System.out.println(p1 instanceof List<?>);//false
System.out.println(p1 instanceof List<Person>);//编译报错

简单来说就是:如果 obj 不为 null 并且 (T) obj 不抛 ClassCastException 异常则该表达式值为 true ,否则值为 false 。

所以对于上面提出的问题就很好理解了,为什么 p1 instanceof String 编译报错,因为(String)p1 是不能通过编译的,而 (List)p1 可以通过编译。

obj 为 class 类的直接或间接子类

public class Man extends Person{
     
}

Person p1 = new Person();
Person p2 = new Man();
Man m1 = new Man();
System.out.println(p1 instanceof Man);//false
System.out.println(p2 instanceof Man);//true
System.out.println(m1 instanceof Man);//true

注意第一种情况, p1 instanceof Man ,Man 是 Person 的子类,Person 不是 Man 的子类,所以返回结果为 false。

作用:判断一个对象是否属于一个类

格式: 对象 instanceof 类  (返回布尔型)

一般用于向下转型做判断

 public class Animal {
     
     public void say(){
         System.out.println("我是一个动物!");
     }
 }
public class Dog extends Animal{
    
    @Override
    public void say() {
        System.out.println("我是一只狗");
    }

    /**
     * 定义子类方法
     */
    public void f1(){
        System.out.println("狗喜欢啃骨头");
    }
}
public class Cat extends Animal {
    
    @Override
    public void say(){
        System.out.println("我是一只猫");
    }
    
    /**
     * 定义子类方法
     */
    public void f2(){
        System.out.println("猫喜欢吃鱼");
    }
}
public class Test {
    
    /**
     * 静态方法里面 无法调用非静态方法 下面main方法是静态的,所以doSomeThing方法必须要为静态方法
     * @param animal
     */
    public static void doSomeThing(Animal animal){
        animal.say();
        if(animal instanceof Dog){ // 向下转型做判断,这样下面的代码就会是安全的
            ((Dog) animal).f1();
        }
        if(animal instanceof Cat){ // 向下转型做判断,这样下面的代码就会是安全的
            ((Cat) animal).f2();
        }
    }
    
    public static void main(String[] args) {
        Animal dog = new Dog();
        System.out.println("dog对象是否属于Animal类:"+(dog instanceof Animal));
        System.out.println("dog对象是否属于Dog类:"+(dog instanceof Dog));
        System.out.println("dog对象是否属于Cat类:"+(dog instanceof Cat));
    
        doSomeThing(new Dog());
        doSomeThing(new Cat());
        
    }
}

运行结果:

dog对象是否属于Animal类:true
dog对象是否属于Dog类:true
dog对象是否属于Cat类:false
我是一只狗
狗喜欢啃骨头
我是一只猫
猫喜欢吃鱼

 

java的native关键字

   简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。

 在定义一个native method时,并不提供实现体(有些像定义一个java interface),因为其实现体是由非java语言在外面实现的。下面给了一个示例:   

package java.lang; 
public class Object { 
    ......
    public final native Class<?> getClass(); 
    public native int hashCode(); 
    protected native Object clone() throws CloneNotSupportedException; 
    public final native void notify(); 
    public final native void notifyAll(); 
    public final native void wait(long timeout) throws InterruptedException; 
    ......
} 

       标识符native可以与所有其它的java标识符连用,但是abstract除外。这是合理的,因为native暗示这些方法是有实现体的,只不过这些实现体是非java的,但是abstract却显然的指明这些方法无实现体。native与其它java标识符连用时,其意义同非Native Method并无差别。

    一个native method方法可以返回任何java类型,包括非基本类型,而且同样可以进行异常控制。这些方法的实现体可以自制一个异常并且将其抛出,这一点与java的方法非常相似。
    native method的存在并不会对其他类调用这些本地方法产生任何影响,实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法。JVM将控制调用本地方法的所有细节。

    如果一个含有本地方法的类被继承,子类会继承这个本地方法并且可以用java语言重写这个方法(这个似乎看起来有些奇怪),同样的如果一个本地方法被fianl标识,它被继承后不能被重写。
   本地方法非常有用,因为它有效地扩充了jvm.事实上,我们所写的java代码已经用到了本地方法,在sun的java的并发(多线程)的机制实现中,许多与操作系统的接触点都用到了本地方法,这使得java程序能够超越java运行时的界限。有了本地方法,java程序可以做任何应用层次的任务。

为什么要使用Native Method
   java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。
   与java环境外交互:
   有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。
   与操作系统交互:
   JVM支持着java语言本身和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
    Sun's Java
    Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread 的 setPriority()方法是用java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 SetPriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。

JVM怎样使Native Method跑起来:
    我们知道,当一个类第一次被使用到时,这个类的字节码会被加载到内存,并且只会回载一次。在这个被加载的字节码的入口维持着一个该类所有方法描述符的list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public之类)等等。
    如果一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。这些实现在一些DLL文件内,但是它们会被操作系统加载到java程序的地址空间。当一个带有本地方法的类被加载时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。当本地方法被调用之前,这些DLL才会被加载,这是通过调用java.system.loadLibrary()实现的。
   最后需要提示的是,使用本地方法是有开销的,它丧失了java的很多好处。如果别无选择,我们可以选择使用本地方法。

 

java的transient关键字

Java 中的 transient 关键字被用来表示变量将不被序列化处理。

序列化是对象进行持久化处理,也就是说,将对象转化成一个字节流进行存储(比如存储为一个字节文件)或传输(通过网络传输字节)。同时,我们也可以从字节中反序列化一个对象出来。这是Java程序中一个重要的概念,因为网络应用中通常需要将对象序列化成字节传输。每一个需要序列化的对象,都要实现 Serializable 接口。

默认情况下,对象所有的变量都会转变成持久状态。但是有时候,一些变量可能不需要序列化,因为没必要对这些变量进行序列化。这时,你就可以将这些变量申明为 transient。如果一个变量被申明为 transient,那么这个变量就不会被序列化。

public class NameStore implements Serializable {
    private String firstName;
    private transient String middleName;
    private String lastName;

    public NameStore (String fName, String mName, String lName){
        this.firstName = fName;
        this.middleName = mName;
        this.lastName = lName;
    }

    @Override
    public String toString() {
        return "NameStore{" +
                "firstName='" + firstName + '\'' +
                ", middleName='" + middleName + '\'' +
                ", lastName='" + lastName + '\'' +
                '}';
    }
}

public class TransientExample {
    public static void main(String[] args) throws Exception {
        NameStore nameStore = new NameStore("Steve", "Middle","Jobs");
        ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("nameStore"));
        // writing to object
        o.writeObject(nameStore);
        o.close();

        // reading from object
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("nameStore"));
        NameStore nameStore1 = (NameStore)in.readObject();
        System.out.println(nameStore1);
    }
}

middleName 变量被申明为 transient,因此没有被序列化。

NameStore{firstName='Steve', middleName='null', lastName='Jobs'}

 

java的strictfp关键字

strictfp, 即 strict float point (精确浮点)。

自Java2以来,Java语言增加了一个关键字strictfp。strictfp的意思是FP-strict,也就是说精确浮点的意思。在Java虚拟机进行浮点运算时,如果没有指定strictfp关键字时,Java的编译器以及运行环境在对浮点运算的表达式是采取一种近似于我行我素的行为来完成这些操作,以致于得到的结果往往无法令你满意。而一旦使用了strictfp来声明一个类、接口或者方法时,那么所声明的范围内Java的编译器以及运行环境会完全依照浮点规范IEEE-754来执行。因此如果你想让你的浮点运算更加精确,而且不会因为不同的硬件平台所执行的结果不一致的话,那就请用关键字strictfp。

你可以将一个类、接口以及方法声明为strictfp,但是不允许对接口中的方法以及构造函数声明strictfp关键字,例如下面的代码:

1. 合法的使用关键字strictfp

strictfp interface A {}
public strictfp class FpDemo1 {
    strictfp void f() {}
}

2. 错误的使用方法

interface A {
    strictfp void f();
}
public class FpDemo2 {
    strictfp FpDemo2() {}
}

一旦使用了关键字strictfp来声明某个类、接口或者方法时,那么在这个关键字所声明的范围内所有浮点运算都是精确的,符合IEEE-754规范的。例如一个类被声明为strictfp,那么该类中所有的方法都是strictfp的。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

wespten

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

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

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

打赏作者

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

抵扣说明:

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

余额充值