3183 2018-04-03 2020-06-25

前言:就不写了,直接看吧。

一、基本概念

1、形参和实参

在Java中,定义一个方法的语法格式如下

  • 修饰符】 返回值类型 方法名(【形参1, 形参2, … 形参n】){ 方法体 }

而调用一个方法的语法格式如下

  • 方法名(【实参1, 实参2, … 实参n】)

实参和形参的传递广义上上包含传值传址两种方式,有几点需要注意:

  • 实参要和形参参数类型、数量相对应(符合自动转换机制也可)
  • 传值方式是将实参的值复制一份传递给形参(当实参是基本数据类型时),方法调用时,形参值的改变不会影响实参值
  • 传址方式是指将实参的地址传递个形参(当实参是非基本数据类型时),即形参和实参指向同一内存空间,形参值的改变会引起实参的同样改变(后面有更深入的讨论)

2、延伸至数组

看代码

static class Test {
    int value;
    Test(int value) {
        this.value = value;
    }
}
static final class Test1 {
    Integer value;
    public Test1(int value) {
        this.value = value;
    }
}

public static void main(String[] args) {
    int a = 1, b = 2, c = 3, d = 4;
    int[] test = {a, b, c, d};
    test[0] += 1;
    System.out.print(test[0] + " ");
    System.out.println(a);// 2 1

    String aa = "1", bb = "2", cc = "3";
    String[] test1 = {aa, bb, cc};
    test1[0] = "11";// 想当于new了一个对象
    System.out.print(test1[0] + " ");
    System.out.println(aa);// 11 1

    Test aaa = new Test(1), bbb = new Test(2), ccc = new Test(3);
    Test[] test2 = {aaa, bbb, ccc};
    test2[0].value += 1;
    System.out.print(test2[0].value + " ");
    System.out.println(aaa.value);// 2 2

    Test1 aaaa = new Test1(1), bbbb = new Test1(2), cccc = new Test1(3);
    Test1[] test3 = {aaaa, bbbb, cccc};
    test3[0] = new Test1(11);
    System.out.print(test3[0].value + " ");
    System.out.println(aaaa.value);// 11 1同String
    
    Integer temp1 = new Integer(10);
    Integer temp2 = new Integer(20);
    test3[1].value = temp1;
    temp1 = temp2;
    System.out.print(test3[1].value + " ");
    System.out.println(temp1);// 10 20这一步需要好好理解
}

3、值传递(常考)

在程序设计语言中,将参数传递分为按值调用按引用调用。按值调用:表示方法接收的是调用者提供的值。而按引用调用表示方法接收的是调用者提供的变量地址一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。

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

public class Example {
    String str = new String("good");
    char[] ch = { 'a', 'b', 'c' };
    public static void main(String[] args) {
        Example ex = new Example();
        ex.change(ex.str, ex.ch);
        System.out.print(ex.str + " and ");
        System.out.print(ex.ch);
    }
    public void change(String[] str, char[] ch) {
        str = "test ok";
        ch[0] = 'g';
    }
    // 同理,如果传入int i, i = i + 1或i = 0,也不会生效,原因见下
}

按照前面的逻辑,String是传址,char数组也是传址,结果应该是“test ok and gbc”,但结果却是“good and gbc”。这里引用一下别人的,我觉得他讲得很清楚了。如下:

“大家可能以为Java中String和数组都是对象所以肯定是对象引用,然后就会选D,其实这是个很大的误区:因为在java里没有引用传递,只有值传递。这个值指的是实参的地址的拷贝,得到这个拷贝地址后,你可以通过它修改这个地址的内容(引用不变),因为此时这个内容的地址和原地址是同一地址,但是你不能改变这个地址本身使其重新引用其它的对象,也就是值传递”

这篇写得很不错。总结一下Java中方法参数的使用情况:

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

这里再补充一个例子

public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("A");
    list.add("B");
    List<String> list1 = exchange(list);
    System.out.println(list.size());
    System.out.println(list1.size());
}

public static List<String> exchange(List<String> list) {
    list.add("C");
    list.add("D");
    list = new ArrayList<>();
    list.add("E");
    list.add("F");
    return list;
}
// 结果为4 2

4、访问权限

具体如下

~同一个类同一个包不同包子类不同包的非子类
private
无修饰符
protected
public

5、重载

Java中方法的重载机制是匹配方法名和匹配参数特征(参数个数或参数类型不同),与方法的返回值无关(尽管不能定义两个仅返回值不同的函数)。

6、继承

子类拥有父类非private的属性,方法

7、构造方法

使用构造方法时,需要注意以下几点:

  • 一个构造方法调用另一构造方法,只能使用this,只能位于第一句,且只能出现一次
  • 不能在一个类的两个构造方法中使用this相互调用
  • 当父类没有显式声明无参构造方法,且显式声明有参构造方法,那么子类构造方法中第一行需显式调用父类的构造方法(子类实例化时,会先实例化父类,默认是父类的无参构造 )

8、内部类

内部类分为:成员内部类局部内部类匿名内部类静态内部类。其中

  • 成员内部类可以访问外部类的私有变量和方法,修饰符可以为public,实例化时是外部类.内部类 x= new 外部类实例化对象.new 内部类
  • 局部内部类等同于局部变量,但不能拥有修饰符(如public、protected、private、static)
  • 匿名内部类也可以访问外部类的私有变量和方法
  • 静态内部类的实例化不依赖外部类的实例化,它的实例化方式是外部类.内部类 x = new 外部类.内部类

下面是一段测试代码

public class InnerClassTest {

    public void test () {
        class TempClass {
        }
    }

    public void testAnno() {
        new Thread(new Runnable() {
            @Override
            public void run() {
            }
        });
    }

    static class StaticClass {
    }

    class MemberlClass {
    }

    public static void main(String[] args) {
        new InnerClassTest().test();
    }
}

运行一次后,编译目录下有如下相关.class文件:

  • InnerClassTest$1TempClass.class(局部内部类)
  • InnerClassTest$1.class(匿名内部类)
  • InnerClassTest$MemberlClass(成员内部类)
  • InnerClassTest$StaticClass(静态内部类)
  • InnerClassTest(原始类)

值得一提的是,使用javap -verbose是找不到第一项和第二项的,第三项和第四项虽然找到了,但也是转到了第五项。

二、关键字

1、break和continue

break语句和continue语句都只作用于当前循环语句。对于嵌套循环,如果break和continue包含在内层循环中,仅对内层循环有效,而对外层循环无效。外层循环同理。

2、static

static关键字用来修饰类的成员,如成员变量、成员方法及代码块,分别称为静态成员变量(或类成员变量)、静态成员方法(或类成员方法)、静态代码块。

  • 被static修饰的类成员,都会在类加载时,自动开辟属于自己的内存空间,而无需等到类实例化时才开辟空间
  • static方法无法访问一般的成员变量、方法,原因是一般的成员变量、方法需等到类实例化时才开辟内存空间
  • 由于类只加载一次,因此静态代码块里面的代码只会被执行一次
  • 普通代码块与构造器是绑定的,且普通代码块会在构造器之前执行

3、abstract

  • 抽象类不一定有抽象方法,但是有抽象方法就一定是抽象类或接口
  • 抽象方法默认的修饰符是public,不能为private,方法没有方法体(没有大括号)
  • 子类必须实现父类的抽象方法

4、interface

  • 接口的方法默认是public abstract,接口修饰符必须为public(默认),方法没有方法体
  • 接口中不可以定义变量,即只能定义常量(加上final修饰就会变成常量)。所以接口的属性默认是public static final常量(可以不显式声明修饰符,且只能是public),必须赋初始值
  • 接口只是对一类事物的属性和行为更高层次的抽象。对修改关闭,对扩展(不同的实现 implements)开放,接口是对开闭原则的一种体现

5、transient

  • transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中
  • 一个静态变量不管是否被transient修饰,均不能被序列化

三、进阶概念

1、类加载(常考)

通过两个例子来说明,如下

class HelloA {
    public HelloA() {
        System.out.println("HelloA");
    }  
    { System.out.println("I'm A class"); }  
    static { System.out.println("static A"); }

}
public class HelloB extends HelloA {
    public HelloB() {
        System.out.println("HelloB");
    }    
    { System.out.println("I'm B class"); }   
    static { System.out.println("static B"); }
    
    public static void main(String[] args) { 
     new HelloB(); 
   }
}
输出结果如下
static A
static B
I'm A class
HelloA
I'm B class
HelloB

第二个例子更加容易理解一些。

class HelloA {
    public HelloA() {
        System.out.println("HelloA");
    }    
    { System.out.println("I'm A class"); }    
    static { System.out.println("static A"); }
}

public class HelloB extends HelloA {
    public HelloB() {
        System.out.println("HelloB");
    }    
    { System.out.println("I'm B class"); }    
    static { System.out.println("static B"); }    
    public static void main(String[] args) {
        System.out.println("-------main start-------");
        new HelloB();
        new HelloB();
        System.out.println("-------main end-------");
    }
}
输出结果如下
static A
static B
-------main start-------
I'm A class
HelloA
I'm B class
HelloB
I'm A class
HelloA
I'm B class
HelloB
-------main end-------
//(本例中,main方法是子类的一个方法)

这题考查了静态语句块、构造语句块/普通代码块(就是只有大括号的那块)以及构造函数的执行顺序,即一个对象的初始化顺序。下面是总结:

  • 首先加载包含main方法的那个类,这里就是HelloB
  • 由于HelloB的父类HelloA并没有加载,所以先要加载HelloA
  • 首先从上往下执行static语句代码,当static语句执行完之后,即类已被加载(注意一个类只会被加载一次)
  • 当调用构造方法时,将依次执行构造代码块、构造器(两者可以说绑定在一起)

这里再补充一个例子

public class Main {
    int a = 1;
    static int b = 2;
    B ab = new B();
    static {
        System.out.println("执行静态代码,b=" + ++b);
    }

    {
        System.out.println("执行构造代码块,a=" + ++a + ",b=" + ++b);
    }
    public Main() {
        System.out.println("执行构造方法,a=" + ++a + ",b=" + ++b);
    }

    static class A {
        static {
            System.out.println("初始化A");
        }
    }

    static class B extends A {
        static {
            System.out.println("初始化B");
        }
    }

    public static void main(String[] args) {
        new Main();
        new Main();
    }
}
// 下面为结果
执行静态代码,b=3
初始化A
初始化B
执行构造代码块,a=2,b=4
执行构造方法,a=3,b=5
执行构造代码块,a=2,b=6
执行构造方法,a=3,b=7

2、反射

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。

  • 获取Class对象

获取Class对象有下面几种:class.getClass()、Class.forName(str)、class.getClassLoader.loadClass(str)。

  • 后两者的区别

这一点比较容易混淆,Class.forName()会触发类加载的初始化阶段,而class.getClassLoader.loadClass(str)不会触发类的初始化,只是把连接阶段完成。 可以参考下面的示意图

类的生命周期

下面是一些测试代码

static class A {
    static {
        System.out.println("A static");
    }
}

static class B {
    static {
        System.out.println("B static");
    }
}

private static void testClassLoad() throws Exception {
    Class a = Class.forName("site.xiaokui.common.hk.basic.ClassLoader$A");
    System.out.println("class a init");
    Class b = a.getClassLoader().loadClass("site.xiaokui.common.hk.basic.ClassLoader$B");
    System.out.println("class b not init");
    b.newInstance();
    System.out.println("class b init");
}
// 下面是输出
A static
class a init
class b not init
B static
class b init

下面是一些常用的反射代码

static class Father1 {
    private int father = 0;
    protected int father1 = 1;
    int father2 = 2;
    public int father3 = 3;
    private void father() {
    }
    void father1 () {
    }
    protected void father2() {
    }
    public void father3() {
    }
}

static class Son1 extends Father1 {
    private int son = 0;
    protected int son1 = 1;
    int son2 = 2;
    public int son3 = 3;
    private void son() {
    }
    void son1() {
    }
    protected void son2() {
    }
    public void son3() {
    }
}

public static void testReflect() {
    Class<?> cls = Son1.class;
    Field[] fields = cls.getDeclaredFields();
    System.out.println("本类声明的所有字段(包含private)");
    for (Field f : fields) {
        System.out.println(f);
    }
    System.out.println("本类及从父类继承的字段(只显示public)");
    fields = cls.getFields();
    for (Field f : fields) {
        System.out.println(f);
    }
    System.out.println("本类声明的所有方法(包含private)");
    Method[] methods = cls.getDeclaredMethods();
    for (Method m : methods) {
        System.out.println(m);
    }
    System.out.println("本类及从父类继承的方法(只显示public)");
    methods = cls.getMethods();
    for (Method m : methods) {
        System.out.println(m);
    }
}

对于平时反射用得多的情况下,熟悉一下反射的使用是很有必要的。

总访问次数: 255次, 一般般帅 创建于 2018-04-03, 最后更新于 2020-06-25

进大厂! 欢迎关注微信公众号,第一时间掌握最新动态!