JavaSE必备知识点2

JavaSE必备知识点—杂七杂八

递归

基本思想就是自己调用自己。但是如果没有结束的方式,只会循环到死。所以我们要定义递归公式和结束条件。

缺点:递归调用会占用大量的系统堆栈,内存消耗多,在递归调用层次多时速度要比循环慢得多,所以在使用递归的时候要慎重。

面向过程和面向对象

都是解决问题的思维方式,都是代码组织的方式,解决简单问题可以使用面向过程,解决复杂问题:宏观上使用面向对象把握,微观处理上仍然是面向过程。

类内存分析

想要查看具体分析请点击上方链接。类中通常会有属性和方法 。Java虚拟机的内存可分为三个区域:栈stack、堆heap、方法区method area

栈的特点如下:

  • 栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧(存储局部变量、操作数、方法出口等)。若在一个方法中再调用另一个方法,就将下一个方法继续入栈,而且在栈中当前方法并不会关闭,然后等全部方法执行问完后,从上向下弹,后进先出嘛。
  • JVM为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变量等)
  • 栈属于线程私有,不能实现线程间的共享!
  • 由系统自动分配,速度快!栈是一个连续的内存空间

堆的特点如下:

  • 用于存储创建好的对象和数组(数组也是对象)。
  • JVM只有一个堆,被所有线程共享。
  • 是一个不连续的内存空间,分配灵活,速度慢。
  • 当使用new关键字的时候,也就意味着在堆中已经开辟好了一块内存。

方法区(又叫静态区)的特点如下:

  • JVM只有一个方法区,被所有线程共享!
  • 方法区实际上也是堆,只是用于存储类、常量相关的信息。
  • 用来存放程序中永远是不变或唯一的内容(代码、类信息[Class对象]、静态变量、字符串常量等)。

构造方法

构造器也叫构造方法(constructor),用于对象的初始化。要点如下:

  • 通过new关键字进行调用。

  • 构造器虽然有返回值,但是不能够定义返回值类型 (返回值类型肯定是本类),不能在构造器里使用return返回某个值。

  • 如果我们没有定义构造器,则编译器会自动定义一个无参的构造函数。如果已定义则编译器不会自动添加!

  • 构造器的方法名必须和类名一致!

  • 构造方法经常需要重载,而且重载方式和普通方法几乎没有区别。

  • 如果方法构造中形参名与属性名相同的时候,需要使用this关键字区分属性和形参。下面给出示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public class Welcome {
    int id;
    int age;
    String sname;
    Computer comp; /*这个也相当于是一个普通的数据类型嘛*/

    void play(){
    System.out.println("我玩游戏电脑的品牌为:"+comp.brand);
    }
    void sleep(){
    System.out.println("Good night!");
    }
    Welcome(int id,int age){
    this.id = id;
    this.age = age;
    }
    Welcome(){

    }
    Welcome(String sname){
    this.sname = sname;
    }
    public static void main(String[] args){
    Welcome ut = new Welcome();//调用Welcome的构造方法,用于创建对象

    Computer s1 = new Computer();
    s1.brand = "华硕";
    ut.comp = s1;
    ut.play();

    Welcome ut2 = new Welcome(10086,18);
    System.out.println(ut2.id);

    Welcome ut3 = new Welcome("pjl");
    System.out.println(ut3.sname);
    }


    }
    class Computer{
    String brand;
    }
  • 构造方法的第一句总是调用super(),不必要显式的写出来。

垃圾回收机制

垃圾回收机制保证可以将“无用的对象”进行回收。

  • 引用计数法 (循环引用 无法识别)
  • 引用可达法(跟搜索算法)

通用的分代垃圾回收机制:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。我们将对象分为三种状态:年轻态、年老态、持久态。JVM将堆内存划分为Eden、Survivor(2个)和Tenured/Old空间。

1
System.gc();  //向系统请求是否可以进行垃圾回收,至于最终是否执行有系统决定。

尽量不要启动FullGC,代价很大,实在不清楚的请点击上方链接进行复习。

虽然Java存在垃圾自动回收机制,但是仍然会出现内存泄漏的情况。

创建对象

  1. 分配对象空间,并将对象成员变量初始化为0或空。
  2. 执行属性值的显式初始化。
  3. 执行构造方法。 //在这之前对象已经创建好了,所以可以使用this关键字。
  4. 返回对象的地址给相关的变量。

This

this的本质就是“创建好的对象的地址”。由于在构造方法调用前,对象已经创建。因此,在构造方法中也可以使用this代表“当前对象”。

作用:

  • 区分成员变量和局部变量,避免二义性的出现。普通方法中,this总是指向调用该方法的对象。构造方法中,this总是指向正要初始化的对象。
  • 使用this关键字调用重载的构造方法,避免相同的初始化代码。但只能够在构造方法中使用,并且构造器的调用必须位于第一句
  • this不能用于static方法中。

static关键字

static修饰的成员变量和方法从属于类。

普通变量和方法从属于对象。

在类中,用static声明的成员变量为静态成员变量,也成为类变量。类变量的生命周期和类相同,在整个应用程序执行期间都有效。

使用时直接使用类名进行调用即可!!!!

可在非静态方法中调用静态的方法,但是不允许在静态方法中调用非静态方法,这应该很容易理解。

this也不能用于静态方法中,因为静态方法是属于类的,this指的是当前对象。

静态初始化块

构造方法用于对象的初始化,而静态初始化用于类的初始化操作,当然,也仅限于static成员。格式如下:

1
2
3
4
//位置是放在要初始化的类当中。
static{
//内部填写相关的类的初始化内容。
}

Java参数传参机制

Java方法中所有的参数都是”值传递“,也就是“传递的是值的副本”。但是并不代表原值不会发生改变,因为当传入的值是地址时,就有能力去改变(而非必须)。

包机制是Java中管理类的重要手段。开发中,我们会遇到大量同名的类,通过包我们很容易对解决类重名问题,也可以实现对类的有效管理。包对于类,相当于文件夹对于文件的作用。要点入下:

  • 通常作为类的第一句非注释性语句。

  • 包名:可任意,约定俗成为域名倒着写,再加上模块名,便于内部管理类。

  • 写项目的时候一定要加包,不要使用默认包

  • 同一个包中不能够有同名的类。

  • com.x1aolin 与com.x1aolin.aa 这两个包没有包含关系,是两个完全独立的包,只是逻辑上看起来后者是前者的一部分。

  • 1
    package com.x1aolin;  //设置包名,一般放在文档注释之外的第一句

导入包

  • 精确包导入优先!

  • 准确说是导入包中的类。

  • 1
    2
    import com.x1aolin.Welcome;  //在其他位置使用该包内的Welcome类
    import com.x1aolin.*; //在其他位置使用该包内所有的类,会略微降低编译速度,不影响运行速度。
  • 1
    2
    //当然,少量使用的时候,可在要使用的那个类前加上包名
    com.x1aolin.Welcome wel = new com.x1aolin.Welcome();
  • 当导入的包中仍有重名,干脆就用上面在使用类前加包的方法,这样是最精确的,也不容易混淆。

静态导入

用于导入指定类当中的静态属性,这样我们就可以直接使用静态属性了。

1
2
import static java.lang.Math.*;  //导入Math类的所有静态属性
import static java.lang.Math.PI; //导入Math类的PI静态属性

继承

使用关键词 extends 进行使用。

  • Java中类只有单继承,没有多继承;但是接口有多继承。
  • 子类继承父类,可以得到父类的全部属性和方法(除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)。
  • 如果定义一个类时,没有调用extends,则它的父类是:java.lang.Object
  • instanceof 二元运算符,左边是对象,右边是类;当对象是右面类或子类创建的对象时,返回true,否则,返回false

方法重写override

这里和上面方法的重载完全没有相似之处,这里指的是 子类重写从父类中继承过来的方法。子类通过重写父类的方法,可以用自身的行为替换父类的行为。执行时就执行子类方法中的动作,忽略父类的动作,也就是子类重写方法后将父类方法覆盖掉了。当然,也仅限子类创建对象使用该方法有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.x1aolin;

public class TestOverride {
public static void main(String[] args) {
Horse h = new Horse();
System.out.println(h instanceof Vehicle);
h.run();
}
}

class Vehicle{
public void run(){
System.out.println("跑...");
}
public void stop(){
System.out.println("停止");
}
}

class Horse extends Vehicle{
//重写run方法
public void run(){
System.out.println("还是跑,哈哈哈哈哈");
}
}

要点:

  • “==” :方法名、形参列表相同。
  • “<=” :返回值类型和声明异常类型,子类小于等于父类。
  • “>=” :访问权限,子类大于等于父类。

Object类

Object类是所有Java类的根基类,也就意味着所有的Java对象都拥有Object类的属性和方法。

toString()方法

找到Object.class源码下的toString()方法,发现定义如下:

1
2
3
4
//为重写前的方法可以查看自身所在包,类 和创建对象在JVM的堆中地址。 
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

既然我们子类可以重写父类的方法,并且Object类是所有类的父类,所以我们在使用时就可以重写父类的toString()方法。

==和equals方法

==代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象。

equals方法在Object类中的描述如下:

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

由上可以看出,equals提供了定义”对象内容相等“的逻辑。我们也经常会去重写该方法,事实上,我们常用的字符串类String也对该方法进行了重写。

1
2
3
4
5
6
7
8
9
package com.x1aolin;
public class TestEquals {
public static void main(String[] args) {
String a = new String("asd");
String b = new String("asd");
System.out.println(a==b); //理论不相等,因为二者不是同一个对象
System.out.println(a.equals(b)); //理论上相等,因为String类对equals()方法进行了重写
}
}

由上可以引出,重写是十分普遍的,需要根据我们需要的内容进行修改。

super

super是直接父类对象的引用。可以通过super来访问父类中被子类覆盖的方法或属性。也就是我们可以使用重写之前父类中的方法。

使用super调用普通方法,语句没有限制,可以在子类中随便调用。

若是构造方法的第一行代码没有显式的调用super(...)或者this(...),那么Java默认都会调用super(),含义是调用父类的无参数构造方法,这里的super()可以省略。

super(…) //调用父类的含参构造函数

this(…) //调用同类中其他的含参构造函数

继承树追溯

构造方法调用顺序

构造方法的第一句总是:super(...)来调用父类对应的构造方法。所以,流程就是:先向上追溯到Object,然后依次向下执行类的初始化模块和构造方法,知道当前子类为止。

静态初始化块调用顺序(同上)

静态初始化原理同上,上述到Object类,先执行Object的静态初始化块,再向下执行子类的静态初始化块,直到我们的类的静态初始化块为止。

属性/方法查找顺序

以查找变量h为例:先查找当前类中有没有属性h,依次上溯到每个父类,查看每个父类中是否有h,直到Object,如果没有找到,则出现编译错误;上面步骤,只要找到h变量,则这个过程终止。

封装

封装就是把对象的属性和操作结合为一个独立的整体,并尽可能隐藏对象的内部细节。做到“高内聚,低耦合”,高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合就是仅暴露少量的方法给外部使用,尽量方便外部调用。

访问控制符

修饰符 同一个类 同一个包 子类 所有类
private *
default * *
protected * * *
public * * * *
  • private 表示私有,只有自己的类能够访问。
  • default 表示没有修饰符限制(什么都不加就是default,也必须不加,即不存在用default修饰的情况),只有同一个包中的类能够访问。
  • protected 表示可以被同一个包中的类以及其他包中的子类进行访问。
  • public 表示该项目中所有包中的所有类都能够进行访问。

一般情况下,我们会将类中所有的属性统一设置为私有private属性,然后通过设置对应的set()get()方法(它们一般是public属性的)与外界进行联系,并且eclipse向我们设置了快捷方式来帮助我们进行快速设置。然后我们可以在其基础上更新自己的要求。最后,对于boolean属性对应的是is()get()方法。

多态

多态是指同一个方法调用,由于对象不同可能会有不同的行为。

要点:

  • 多态是方法的多态,不是属性的多态(多态与属性无关)。
  • 多态的存在有三个必要条件:继承,方法重写,父亲引用指向子类对象。
  • 父亲引用指向子类对象后,用该父亲引用调用子类重写的方法,此时多态就出现了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.x1aolin;

public class Animal {
public void shout(){
System.out.println("动物叫声!");
return;
}
public static void main(String[] args) {
Animal a = new Dog(); //父亲引用指向子类对象
AnimalCry(a);
Animal b = new Cat(); //父亲引用指向子类对象
AnimalCry(b);
}
public static void AnimalCry(Animal a){ //静态方法,从属于Animal类,不属于任何一个对象
a.shout();
}
}
class Dog extends Animal{
public void shout(){
System.out.println("汪汪汪...");
}
}

class Cat extends Animal{
public void shout(){
System.out.println("喵喵喵...");
}
}

运行结果:

1
2
汪汪汪...
喵喵喵...

拿上述代码中的a对象举个例子,我们虽然在创建对象,使用构造函数的时候使用的是Dog(),使其指向了子类对象,导致它可以使用一些子类重写父类的一些方法。但归根结底它是Animal类的对象,因此,它不能够使用子类中其他的方法,若想使用就用到了,下节的对象的转型。

对象的转型casting

父亲引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。

向上转型后的父亲引用变量只能调用它编译类型的方法,不能调用它运行时类型的方法。这时,我们就需要进行类型的强制转换,我们称之为向下转型!

向下转型,只能将那些使用向上转型的引用变量强制转型回去!!!不能够乱转!举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.x1aolin;

public class Animal {
public void shout(){
System.out.println("动物叫声!");
return;
}
public void sayHello(){
System.out.println("Hello");
}
public static void main(String[] args) {
Animal a = new Dog();
AnimalCry(a);
Dog b = (Dog)a; //强制向下转型,然后就可以调用sayHaha()了。
b.sayHaha();
b.sayHello();
}
public static void AnimalCry(Animal a){
a.shout();
}
}
class Dog extends Animal{
public void shout(){
System.out.println("汪汪汪...");
}
public void sayHaha(){
System.out.println("Haha...");
}
}

运行结果:

1
2
3
汪汪汪...
Haha...
Hello

Final

作用与使用方法如下:

  • 修饰变量:被他修饰的变量不可改变,一旦赋了初值,就不能够被重新赋值。

    1
    final int MAX_VALUE = 100; //一般常量使用大写和下划线的形式
  • 修饰方法:该方法不能被子类重写,但是可以被重载!

    1
    final void study(){}
  • 修饰类:修饰的类不能被继承。比如:Math,String等。

    1
    final class A{}

数组

长度确定,大小在创建的那一刻就被定下来了。其元素必须是相同类型,但类型不限,基本类型和引用类型都可以。数组变量属于引用类型,本质上就是对象,数组中的每个元素相当于该对象的成员变量。

数组声明

1
2
type[] arrName;  //推荐
type arrName[];

声明的时候并没有实例化对象,只有在实例化数组时,JVM才分配空间,这时才于长度有关。

1
2
3
4
5
//基本类型
int[] a = new int[10];
double[] b = new double[8];
//引用类型
Animal[] c = new Animal[4]; //此时相当于分配了

请注意引用类型的情况,下面给出了一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.x1aolin;

public class Test01 {
public static void main(String[] args) {
Animal[] aa = new Animal[4]; //这里仅创造了一个4位指向Animal对象引用(地址)的数组,初值为null
System.out.println(aa[0]==null);
aa[0] = new Animal(3); //只有到了这里才会创建对象并存入真的对象地址
System.out.println(aa[0]==null);
System.out.println(aa[0].getA());
}
}
class Animal{
private int a;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
Animal(int a){
this.a = a;
}
}

数组初始化

静态初始化

1
2
int[] a = {1,2,3};  //数组大小与数组数值直接确定
Animal[] b = {new Animal(1),new Animal(2),new Animal(3)};

默认初始化:因为数组也是对象嘛,所以赋值的规则和成员变量默认规则一致。{0 false null}

动态初始化

就是先声明,后赋值。可以一个一个赋值,也可以使用循环赋值。

for-each循环

专门用于读取数组或集合中所有的元素,即对数组进行遍历。只能够读取,不能够修改!

1
2
3
for(int m:a){ //冒号前m是一个与数组同类型的临时变量,冒号后a表示当前的数组
System.out.println(m); //无下标,不能够精确定位数组的值,因此不能修改。
}

抽象方法与抽象类

抽象方法使用abstract修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体实现。

抽象类包含抽象方法的类。通过abstract方法定义规范,然后要求子类必须通过重写定义具体实现。通过抽象类,我们就可以做到严格限制子类的设计,是子类之间更加通用。

要点:

  1. 有抽象方法的类只能定义成抽象类。
  2. 抽象类不能实例化,既不能用new来实例化抽象类。
  3. 抽象类可以包含属性、方法、构造方法。但是构造方法不能用来new实例,只能用来被子类调用。
  4. 抽象类只能用来被继承。
  5. 抽象方法必须被子类实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.x1aolin;
/**
* 抽象类的意义就在于:为子类提供统一的、规范的模板。子类必须实现相关的抽象方法!
* @author x1aolin
*
*/

public abstract class Animal { //抽象类
//第一、父类没有实现。 第二、子类必须实现,否则编译错误
public abstract void shout(); //抽象方法
//在抽象类中调用普通的方法
public void run(){
System.out.println("run()");
}

public static void main(String[] args) {
Animal ani = new Dog();
ani.shout();
}
}

class Dog extends Animal{

public void shout(){
System.out.println("dog.shout()");
}
//子类在重新写完所有抽象父类中的抽象方法后,可以额外定义其他的方法。
public void fly(){
System.out.println("Dog.fly()");
}
}

接口

接口就是比“抽象类”还“抽象”的“抽象类”,可以更加规范的对子类进行约束。全面地专业地实现了:规范和具体实现的分离。

抽象类还可以提供某些具体的实现。接口不提供任何实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了一批具有的公共方法规范。

接口里面不存在私有的东西。具体格式如下:

1
2
3
4
[访问修饰符] interface 接口名 [extends 父接口1,父接口2,...]{
常量定义;
方法定义;
}

接口中的属性只能是常量,总是public static final修饰。不写也是。

接口中的方法只能是抽象方法,总是public abstract修饰。不写也是。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.x1aolin;
/**
* 接口中所有方法都是抽象方法,所有加不加abstract都无所谓。
*
* 接口可以进行多继承
* @author x1aolin
*
*/
public interface MyInterface {
int MAX_AGE = 100; // = public static final a = 100; 常量

void test01(); // = public abstract void test01();
}

class Student implements MyInterface{

@Override
public void test01() {
System.out.println(MAX_AGE);
System.out.println("Student.test01");
}
}

要点:

  • 子类通过implements来实现接口中的规范。
  • 接口不能创建实例,但是可用于声明引用变量类型。
  • 一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是public的。
  • JDK1.7之前,接口中只能包含静态常量、抽象方法,不能有普通属性、构造方法、普通方法。
  • JDK1.8后,接口中包含普通的静态方法。
  • 接口完全支持多继承。和类的继承类似,子接口扩展某个父接口,将会获得父接口中所定义的一切。
  • 面向接口编程

内部类

一般情况,我们把类定义成独立的单元。有些情况下,我们把一个类放在另一个类的内部定义,称为内部类(innerclasses)。

内部类可以使用public、default、protected 、private以及static修饰。而外部顶级类(我们以前接触的类)只能使用public和default修饰。

内部类只是一个编译时概念,一旦我们编译成功,就会成为完全不同的两个类。对于一个名为Outer的外部类和其内部定义的名为Inner的内部类。编译完成后会出现Outer.class和Outer$Inner.class两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类的相同。

内部类的作用:

  1. 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。

  2. 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。 但外部类不能访问内部类的内部属性。

  3. 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方案变得更加完整。

内部类的使用场合:

  1. 由于内部类提供了更好的封装特性,并且可以很方便的访问外部类的属性。所以,在只为外部类提供服务的情况下可以优先考虑使用内部类。

  2. 使用内部类间接实现多继承:每个内部类都能独立地继承一个类或者实现某些接口,所以无论外部类是否已经继承了某个类或者实现了某些接口,对于内部类没有任何影响。

非静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.x1aolin;
/**
* 测试内部类
* @author x1aolin
*
*/
public class TestInnerClass {
public static void main(String[] args) {
//创建一个内部类
Outer.Inner inn = new Outer().new Inner();
inn.prinT(19);
}
}
class Outer{
private int age = 21;
public void testOuter(){
System.out.println("Outer.testOuter()");
}
//成员内部类 非静态成员内部类
class Inner{
private int age = 20;
public void prinT(int age){
System.out.println("外部类private age = "+Outer.this.age);
System.out.println("内部类private age = "+this.age);
System.out.println("局部变量 age = "+age);
}
}
}
  • 非静态内部类必须寄存在一个外部类对象里(属于外部类的对象而不是外部类本身)。因此,如果有一个非静态内部类对象那么一定存在对应的外部类对象。非静态内部类对象单独属于外部类的某个对象。
  • 非静态内部类可以直接访问外部类的成员(包括私有属性),但是外部类不能直接访问非静态内部类成员。
  • 非静态内部类不能有静态方法、静态属性和静态初始化块。(这些都是依托于类本身,而非静态内部类依托于对象)。
  • 外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例。

静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.x1aolin;

public class TestInnerClass {
public static void main(String[] args) {
//创建一个静态内部类对象 不依赖于外部类的对象
Outer.Inner inn = new Outer.Inner();
}
}
class Outer{
//相当于外部类的一个静态成员
static class Inner{
public void look(){
System.out.println("Outer.Inner.look()");
}
}
}
  • 当一个静态内部类对象存在,并不一定存在对应的外部类对象。因此,静态内部类的实例方法不能直接访问外部类的实例方法。
  • 静态内部类看作外部类的一个静态成员。因此,外部类的方法中可以通过:静态内部类.名字的方式访问静态内部类的静态成员,通过new 静态内部类()访问静态内部类的实例。

匿名内部类

适合那种只需要使用一次的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.x1aolin;

public class TestNeibulei {
public static void test01(AA a){ //接口声明引用变量类型
a.aa(); //可以直接使用接口调用方法
}
public static void main(String[] args) {
TestNeibulei.test01(new AA(){ //使用匿名内部类,直接重写接口中定义的方法!!
public void aa(){
System.out.println("TestNeibulei.main(...).new AA() {...}.aa()");
//System.out.println("pp.aa()");
}
});
}
}
interface AA {
public abstract void aa();
}
/*class pp implements AA{
public void aa(){
System.out.println("pp.aa()");
}
}*/

局部内部类(方法内部类)

局部内部类定义在方法的内部,作用域仅限于本方法,成为局部内部类。主要用于解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,就产生了局部内部类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test{
public void show(){
//作用域仅限于该方法
class Inner{
public void fun(){
system.out.println("Hello!");
}
}
new Inner().fun();
}
public static void main(String[] args) {
new Test().show();
}
}

String类

String类对象代表不可变Unicode字符序列,因此,我们可以将String对象成为“不可变对象”。该对象内部的成员变量一旦确定其值,就不可改变。 就像下面String的某些方法,比如:substring()是对字符串的截取操作,但本质是读取原字符串内容生成了新的字符串。

因为有常量池的原因,字符串比较要用equals(),而不是=。其常用功能如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class TestString {
public static void main(String[] args) {
String s1 = "core Java";
String s2 = "Core Java";
//按字典顺序比较两个字符串。 比较是基于字符串中每个字符的Unicode值。
System.out.println(s1.compareTo(s2));
//忽略大小写的字符串比较
System.out.println(s1.compareToIgnoreCase(s2));
//将指定的字符串连接到该字符串的末尾。 链式调用。核心就是:该方法调用了return this,把自己返回了
System.out.println(s2.concat("jjj").concat("hhhhhh"));
//返回从替换所有出现的导致一个字符串oldChar在此字符串newChar 。
String s3 = s2.replace('a','b');
System.out.println(s1.charAt(0)); //提取该字符串中下标为0的字符
System.out.println(s1.length()); //返回字符串长度
System.out.println(s2.equals(s1)); //比较两个字符串是否相等
System.out.println(s2.equalsIgnoreCase(s1));//在忽略大小写的情况下判断是否相等
//字符串s1中是否包含e J,返回指定子字符串第一次出现的字符串内的索引。
System.out.println(s1.indexOf("e J"));
//返回指定字符的最后一次出现的字符串中的索引。
System.out.println(s1.lastIndexOf("e J"));
System.out.println(s1.indexOf("apple"));//字符串s1中是否包含apple,未找到则返回-1
String ss = s2.replace('J', ' '); //s2本身不变,将J替换成空格后 赋值给ss
System.out.println("result is :" + ss);
String s = ""; //设置一个空字符串
String ss1 = "How are you?";
System.out.println(ss1.startsWith("How"));//是否以How开头
System.out.println(ss1.endsWith("you"));//是否以you结尾
s = ss1.substring(4);//提取子字符串:从下标为4的开始到字符串结尾为止
s = ss1.substring(8, 11);//提取子字符串:下标[8, 11) 不包括11
s = ss1.toLowerCase();//转小写
s = ss1.toUpperCase();//转大写
String ss2 = " How old are you!! ";
s = ss2.trim();//去除字符串首尾的空格。注意:中间的空格不能去除
System.out.println(s);
System.out.println(ss2);//因为String是不可变字符串,所以s2不变
//不够八位数前面补零,返回String类型
System.out.println(String.format("%08d", 1001));
}
}

在遇到字符串常量之间的拼接时,编译器会做出优化,即在编译期间就会完成字符串的拼接。因此,在使用==进行String对象之间的比较时,我们需要特别注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestString2 {
public static void main(String[] args) {
//编译器做了优化,直接在编译的时候将字符串进行拼接
String str1 = "hello" + " java";//相当于str1 = "hello java";
String str2 = "hello java";
System.out.println(str1 == str2);//true
String str3 = "hello";
String str4 = " java";
//编译的时候不知道变量中存储的是什么,所以没办法在编译的时候优化
String str5 = str3 + str4;
System.out.println(str2 == str5);//false
}
}

牢记:字符串比较的时候用equals()就行了,一般别用“==”,因为==比较的是存储该字符串的“地址”,也就是比较的是否二者是同一个对象。

StringBuilder与StringBuffer

以上两者与String不同,为可变字符序列。也就是说定义之后可以改变。StringBuilder线程不安全,效率高(一般使用它);StringBuffer线程安全,效率低。下面说一下它们的常用方法,详细的还是找API文档自行观看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestString {
public static void main(String[] args) {
StringBuilder str = new StringBuilder();
int i = 25;
do{
str.append((char)('a'+i)); //字符连成字符串
i--;

}while(i>=0);
System.out.println(str);
str.reverse(); //倒叙
System.out.println(str);
str.setCharAt(2, '林'); //替换
System.out.println(str);
str.insert(0, '我').insert(1, '爱').insert(2, '你'); //插入
System.out.println(str);
str.delete(3, 5); //删除 [3,5) 不包括5
str.deleteCharAt(26); //删除
System.out.println(str);
}
}

可变与不可变字符序列使用陷阱

因为String是不可变字符序列,所以在进行字符串拼接的时候,需要创建一个新的对象进行存储拼接后的字符串,而StringBuilder则没有这个情况,所以当遇到大量字符串累加的时候,一定要使用可变字符序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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));
}
}

数组的拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.x1aolin;

public class TestArrayCopy {
public static void main(String[] args) {
String[] str = {"aa","bb","cc","dd","ee"};
int index = 2;
String[]ss = deleteElment(str,index);//我想搞清楚为什么str1被真实的改变了,而不是其拷贝
for(String m:ss){
System.out.println(m);
}
String in = "ff";
String[] sss = extendArray(in,str,index);
for(String m:sss){
System.out.println(m);
}
}
//删除字符串数组中的一个值,本质上还是数组的拷贝
public static String[] deleteElment(String[] s,int index){
System.arraycopy(s, index+1, s, index, s.length - index-1);
s[s.length-1] = null;
index = 999;
return s;
}
//向数组中插入数值 涉及到数组的扩容,因为字符数组一旦创建就定死了,所以必须新申请数组进行创建。
public static String[] extendArray(String str,String[] s,int index){
String[] ss = new String[s.length+1];
System.arraycopy(s, 0, ss, 0, index);
System.arraycopy(s, index, ss, index+1, s.length-index);
ss[index] = str;
return ss;
}
}

Java.util.Arrays类

自定义类的排序需要实现Comparable接口。

1
public static int binarySearch(Object[] a,Object key)

Tips: 使用二分搜索法来搜索指定数组,以获得指定对象。在进行此调用之前,必须根据元素的自然顺序对数组进行升序排序(通过 sort(Object[]) 方法)。如果没有对数组进行排序,则结果是不确定的。(如果数组包含不可相互比较的元素(例如,字符串和整数),则无法 根据其元素的自然顺序对数组进行排序,因此结果是不确定的。)如果数组包含多个等于指定对象的元素,则无法保证找到的是哪一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.x1aolin;
import java.util.Arrays;
/**
* 测试java,util.Arrays工具类
* @author x1aolin
*
*/
public class TestArrays {
public static void main(String[] args) {
int[] arr = {5,4,3,2,1};
//打印指定数组的内容
System.out.println(Arrays.toString(arr)); //与Object中的toString()方法是两码事,只是名字相同
Arrays.sort(arr); //从小到大排序 后面的不包括
System.out.println(Arrays.toString(arr));
//二分法进行搜索
System.out.println(Arrays.binarySearch(arr, 3));
}
}

多维数组

1
2
3
4
5
6
7
8
int[][] a = new int[3][];  //创建多维数组
a[0] = new int[]{20,30};
a[1] = new int[]{10,11,12};
a[2] = new int[]{55,66};
//静态初始化二维数组
int[][] b = {
{20,30},{10,11,12},{55,66}
};

合理使用多维数组可以存储表格数据,示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestArrays {
public static void main(String[] args) {
//涉及包装类
Object[] arr1 = {10001,"x1aolin",18,"工程师","2016.6.6"};
Object[] arr2 = {10010,"x1aohong",19,"研究员","2016.6.6"};
Object[] arr3 = {10086,"x1aohua",21,"教师","2016.6.6"};

Object[][] b = new Object[3][];
b[0] = arr1;
b[1] = arr2;
b[2] = arr3;

for(Object[] temp:b){
System.out.println(Arrays.toString(temp));
}
}
}

冒泡排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.x1aolin;
import java.util.Arrays;

public class MaoPaoSort {
public static void main(String[] args) {
int[] a = {1,2,3,5,4};
for(int i=a.length-1;i>0;i--){
boolean flag = false; //冒泡排序的优化问题
for(int j=0;j<i;j++){
if(a[j]>a[j+1]){
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
flag = true;
}
System.out.println(Arrays.toString(a));
}
System.out.println("----------");
if(!flag) break;
}
}
}

包装类

Java是面向对象的语言,但并不是“纯面向对象”的,因为我们经常用到的基本数据类型就不是对象。但是我们在实际应用中经常需要将基本数据转化成对象,以便于操作。为了解决这个不足,Java在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类

基本数据类型 包装类
byte Byte
boolean Boolean
short Short
char Character
int Integer
long Long
float Float
double Double

基本数据类型,包装类,字符串的相互转化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.xiaolin.a;
/**
* 测试包装类
* @author x1aolin
*
*/
public class TestWrappedClass {
public static void main(String[] args) {
//基本数据类型转成包装类对象
Integer a = new Integer((int)4.1);
Integer b = Integer.valueOf(4); //调用静态方法 推荐使用
if(a==b){ //使用a.equals(b)就打印“Hello”了
System.out.println("Hello");
}
else{
System.out.println("Goodbye"); //因为a,b是两个不同的对象,所以输出这个
}
//包装类对象转成基本数据类型 设计自动装箱,拆箱问题
int c = b.intValue();
System.out.println(c);

//把数字字符串转换成包装类对象 只能够是数字字符串!!!
Integer d = new Integer("77");
System.out.println(d);

//把包装类对象转化成字符串
String str = d.toString(); //""+b //与空字符串相加也可以转化
System.out.println(str);

//常见常量 其余自己去源码中找
System.out.println(Integer.MAX_VALUE); //21亿多
System.out.println(Integer.MIN_VALUE); //-21亿多
}
}

自动装箱,拆箱

1
2
3
4
5
6
7
8
9
10
public class TestWrappedClass {
public static void main(String[] args) {
Integer a = 12; // -> Integer a = Integer.valueOf(12); 编译器自动装箱
int b = a; // -> int b = a.intValue(); 编译器自动拆箱

Integer c = null;
//编译出错,因为这里本质上是调用对象c的intValue()方法,但是c为空,所以这里会报空指针错误
int d = c;
}
}

包装类的缓存问题

整型、char类型所对应的包装类,在自动装箱时,对于-128~127之间的值会进行缓存处理,其目的是提高效率。缓存处理的原理为:

如果数据在-128~127这个区间,那么在类加载时就已经为该区间的每个数值创建了对象,并将这256个对象存放到一个名为cache的数组(在方法区内部)中。每当自动装箱过程发生时(或者手动调用valueOf()时),就会先判断数据是否在该区间,如果在则直接获取数组中对应的包装类对象的引用,如果不在该区间,则会通过new调用包装类的构造方法来创建对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test3 {
public static void main(String[] args) {
//缓存[-128,127]之间的数字。实际就是系统初始化的时候,创建了[-128,127]之间的一个缓存数组。
Integer in1 = -128;
Integer in2 = -128;
System.out.println(in1 == in2);//true 因为123在缓存范围内 in1 in2是同一个对象,地址相同
System.out.println(in1.equals(in2));//true
Integer in3 = 1234;
Integer in4 = 1234;
System.out.println(in3 == in4);//false 因为1234不在缓存范围内 in3 in4不是同意对象
System.out.println(in3.equals(in4));//true
}
}

时间处理相关类

Date类

在计算机中,我们把1970年1月1日00:00:00定为基准时间(北京时间8点),每个度量单位是毫秒(1秒的千分之一)。我们用long类型的变量表示时间,从基准时间往前几亿年,往后几亿年都可以表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.xiaolin.a;
import java.util.Date;
public class TestDate {
public static void main(String[] args) {
Date d1 = new Date(); //当前时刻
System.out.println(d1);
//从1970年1月1日00:00:00时刻开始所过的毫秒数(可为负)
Date d2 = new Date(2000);
System.out.println(d1.after(d2)); //true
System.out.println(d1.before(d2)); //false
Date d3 = new Date(2000);
System.out.println(d2.equals(d3)); //时间是否相等 true
System.out.println(d1.getTime()); //返回从基准点到当前时间所用的毫秒数
System.out.println(d1.toString()); //转换成字符串形式

}
}

以后遇到日期处理,要使用Canlendar日期类。

DateFormat类

作用:把时间对象转化成指定格式的字符串。也可以将指定格式的字符串转化成时间对象。DateFoemat是一个抽象类,一般使用它的子类SimpleDateFormat类来实现。下表是相应的一些格式,也给出了部分代码示例。

表8-2 格式化字符的含义.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.xiaolin.a;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TestDateFormat {

public static void main(String[] args) throws ParseException {
//后者是前者的子类 特殊标记不要变即可,其余地方可以随便加
//把时间对象转换成字符串
DateFormat d1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.out.println(d1.format(new Date()));

//将字符串按照“格式字符串指定格式”转成相应的字符串
DateFormat d2 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Date date = d2.parse("2016-9-01 20:44:10"); //上面这一行是什么格式,下面这一行就是什么模式
System.out.println(date);
//测试其他
DateFormat d3 = new SimpleDateFormat("D"); //表示今天是今年的多少天
System.out.println(d3.format(new Date()));
}
}

Calendar日期类

Calendar类是一个抽象类,为我们提供了关于日期计算的相关功能,比如:年、月、日、时、分、秒的展示和计算。GregorianCalendar时calendar的一个具体子类,提供了世界上大多数国家/地区使用的标准日历系统。

您的每一份支持将鼓励我继续创作!
-------------本文结束感谢您的阅读-------------