java 学习全程记录

大型 java 从零开始踩坑现场

java 中 static 和 public 的用法

java 中关于类的操作

类的写法

类的写法格式如下所示:

1
2
3
4
5
6
7
8
public class Student{
String name;
int age;
int id;
public study(){
System.out.println("我正在学习!");
}
}

貌似在类的非方法语段只能进行变量的定义操作,其他操作都是不允许的,如果想要进行其他操作则需要使用 static 语段。比说说要打印一段话则需要

1
2
3
static{
System.out.println("....");
}

java 中类的实例化

以上面创建的类为例说明 java 中实例化操作,java 中标准化的实例语句为 Student stu = new Student(); 其中 Student stu 的意思是,在内存中分配一个名字叫做 stu 的变量,这个变量是 Student 类型的。后半部分 new Student() ;这就是利用 new 关键字加上构造方法(所谓构造方法就是在类名后加上一个括号,如Student() ) 来创建一个实例,比如示例代码中创建的就是一个 Student 类的实例。当使用 new 关键字创建一个新的实例后,new 关键字会将新创建的实例在内存中的地址返回,之后 stu 将会接受这个地址,所以 stu 就是Student 类实例的一个引用,而不是实例本身,真正的实例是 new 出来的那个对象。

java 中构造方法及其作用

构造方法就是对类进行初始化,个人理解它是与 python 中的 __init()_ 相似的方法,都是对类中的变量进行初始化,通过初始化将值赋给类中的变量,这样在以后调用类中的数据时可以有效区分类内部的变量和类外部的变量,防止出错。

java 中构造方法的特点:

1.构造方法的名称需要与类的名称一致

2.当类中没有需要初始化的参数时会直接调用自动生成的无参数构造方法,也就是类名加括号;当类中存在需要初始化的参数时,将不会自动生成构造方法,这时就需要自己编写构造方法,根据规范,在自定义含参构造器的同时还需要定义一个不含参的构造器。构造方法内部需要将传入的参数赋给类中的变量。代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Person //人类{
public Person(String n,int a) //构造方法

{
name = n; age = a;
}
private string name;
private int age;
}
static void main(String[] args){
Person p = new Person("张三",14);//这就是作用
}

构造方法也存在重载,这和以前普通方法的重载是一样的

this 的使用场景

1.发生二义性的情况下通过 this 区分局部变量和成员变量,如类的构造方法中传入的参数名和成员变量名相同这是需要使用 this 进行区分

2.在构造方法重载时使用。如:构造方法1对 a 、b 两个变量进行赋值,而构造方法2对 a、b、c三个成员变量进行赋值,这时可以通过语句 this(a,b) 调用另一个构造方法,然后再单独对成员变量 c 进行赋值。需要注意的是 this 语句必须写在构造方法的第一行。

2.不能用于 static 语句中。

static 用法

使用 static 声明的变量和方法可以全局使用,static 代码块会跟随类一起加载,也就是说他会在所有其他操作之前进行,毕竟类初始化加载之后才能对类进行操作,同理,不能在静态方法中调用非静态变量。静态方法可以调用静态方法,究其缘由就是因为静态方法会随着类一起加载,而普通方法则要在类实例化后才能加载,所以静态方法无法找到还没有生成的普通方法也就没法调用(我是这么理解的也不知道对不对)

包的导入

java 包的导入使用 import 关键词,当导入的多个包里存在相同的名称时将会优先使用描述更加详细的那一项。例如 import java.util.Dateimport java.sql.* 中都含有Date方法,这是就会使用 util 中的 Date ,因为它描述的更加详细。

静态导入

使用 import static 表示导入类下面的静态属性,例如导入圆周率 PI ,可以使用 import static java.lang.Math.PIimport static java.lang.Math.*

垃圾回收算法与机制

垃圾回收算法

垃圾回收算法分为计数法和根搜索算法。

计数法存在相互调用的问题故不太稳定。根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点 GC ROOT 开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

垃圾回收机制

java中不同对象的生命周期是不一样的,不同周期对象课采用不同垃圾回收算法,以提高效率,根据对象活跃程度分为年轻代、年老代、持久代。JVM 堆区划分为 Eden、Survivor、Tenured/Old 区。

年轻代
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分为 Eden 区和两个 Survivor (一般为两个,也可多个)。

年老代
在年轻代中经历N次垃圾回收后,仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

持久代
存放静态文件,如 java 类、方法等,对垃圾回收没有显著影响

GC: Garbage Collection 垃圾回收
Minor GC
用于清理年轻代,Eden 满了,就触发 Minor GC ,清理无用对象,把有用对象放到Survivor1或 Survivor2 中。

Major GC
用于清理老年代。

Full GC
清理年轻代、老年代区域,成本高,对系统性能产生影响

清理过程

  1. 创建新对象,大多数放在Eden区

  2. Eden满了(或达到一定比例),触发Minor GC, 把有用的复制到Survivor1, 同时清空Eden区。

  3. Eden区再次满了,出发Minor GC, 把Eden和Survivor1中有用的,复制到Survivor2, 同时清空Eden,Survivor1。

  4. Eden区第三次满了,出发Minor GC, 把Eden和Survivor2中有用的,复制到Survivor1, 同时清空Eden,Survivor2。

    形成循环,Survoivor1和Survivor中来回清空、复制,过程中有一个Survivor处于空的状态用于下次复制的。

  5. 重复多次(默认15),没有被Survivor清理的对象,复制到Old(Tenuerd)区.

  6. 当Old达到一定比例,触发Major GC,清理老年代。

  7. 当Old满了,触发Full GC。注意,Full GC清理代价大,系统资源消耗高。

注:

  1. 程序员可以像JVM提出垃圾回收请求,但是实际是否回收由JVM决定,程序员只提出清理请求建议,不能直接清理。

  2. Full GC清理代价大,系统资源消耗高,很多性能优化是针对Full GC做优化。

参考资料:https://blog.csdn.net/xmj15715216140/article/details/80664630

继承

继承的概念和 python 的一样,现在说一下 java 继承的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person{
String name;
int height;
public void rest(){
System.out.println("我正在休息");
}
}
class Student extends Person{
String major;
public void study(){
System.out.println("一周学七天,一学期学一周");
}
public Student(String name,int height,String major){
this.name = name;
this.height = height;
this.major = major;
}
public static void main(String[] args) {
Student student = new Student("张三",178,"****");

student.rest();
student.study();
}
}

如上所示直接使用 extends 进行继承就行了

子类继承父类可以得到父类的全部属性和方法(除了父类的构造方法),但不见得可以直接访问,比如父类的私有属性和方法。如果定义一个类时,没有调用extends,则它的父类是 java.lang.Object。

在编写构造函数的时候需要注意的是构造函数本身并没有返回值,所以在编写构造函数时不能使用 void 关键字,否则会报错,提示构造函数未被创建!!

可以使用 instanceof 判断两个类之间的继承关系,比如说 Student instanceof Person 返回的就是 True 因为 Student 是Person 的子类。

继承方法的重写

若要改写父类中定义的方法,则需要对方法进行重写,重写的方法是在子类中重新定义一个和父类中方法名一致的方法然后对方法进行自己想要的改写。代码示例:

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 TestOverride {

public static void main(String[] args) {
// TODO Auto-generated method stub
Hourse h = new Hourse();
h.run();

}

}

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

public void stop() {
System.out.println("停!");
}

}

class Hourse extends Vehicle{
public void run() {
System.out.print("四蹄翻飞,嘚嘚嘚...");
}
}

需要注意的是重写方法的返回类型必须小于父类的返回类型,比如说父类返回的是继承于 Object 的,如果重写的返回类型直接就是 Object 类,这样就会出错。

可以基于此对 Object 类的一些方法进行重写以满足自己的需要,例如对 equal 类的重写。Object 的 equal 方法是判断两个类的内容是否一致,但如果我们仅仅只需要类中的其中一个属性来进行判断,这时就可以进行重写。示例如下:

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
public class TestObject {

public static void main(String[] args) {
// TODO Auto-generated method stub
Object obj;
String str;
User u1 = new User("张三",123456,12345);
User u2 = new User("李四",123456,54321);
System.out.println(u1.equals(u2));
System.out.println(u1 == u2);

}

}

class User{
String name;
int id;
int pwd;
public User(String name,int id,int pwd) {
this.id = id;
this.name = name;
this.pwd = pwd;
}
@Override
public boolean equals(Object obj) {//对Object的equal方法进行重写,通过ID判断是否相等

if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (id != other.id)
return false;
return true;
}

}

两个打印语句,第一个打印 True, 第二个打印 False。这是因为 u1 和 u2 是两个不同的对象,故不相等,但两个对象的 ID 是相等的,故改写后的 equal 方法返回 True。equal 方法的改写可以通过 eclipse 右键菜单的 source 选项内功能直接生成。

super 的用法

super 是当父类的方法被重写之后,在子类中调用父类方法的关键字。如父类中一个叫做 f() 的方法,在子类中被重写了,如果想要在子类中重新调用重写之前的方法就需要使用 super.f() 。

此外,super 还用在构造方法之中。每个构造方法的第一句不论写不写都是 super() ,这也就导致了当你 new 一个新对象时,肯定会先运行对应类的父类构造器,层层向上。当使用 super() 传递参数时,只能传递父类构造器中声明的参数,其他的可以使用 super().value = **** 形式进行值传递

类的向上向下转型

当用父类名称声明子类实例时,该实例只能调用子类和父类共有的方法,若有方法在子类中被重写,则调用的是重写后的方法。向下转型使用强制转型方法,转型后可以使用子类独有的方法。例如: Animal 是 Dog 的父类,那么 Animal dog = new Dog() 声明的的 dog 只能使用 Dog 类和 Animal 类共有的方法,如果使用语句 dog = (Dog)dog 将 dog 向下转型为 Dog 类实例,则 dog 可以使用 Dog 类独有的方法。

封装

tips:面向对象的三大特征:继承,封装和多态

高内聚:类的内部数据操作细节自己完成,不允许外部干涉

低耦合:仅暴露少量的方法给外部使用,尽量方便外部调用

访问控制符

private:表示私有,只能自己类访问,子类无法使用父类的私有属性和方法

default:表示没有修饰符修饰,只有同一个包的类能访问

protected:表示可以被同一个包的类以及其他包中的子类访问

public:表示可以被该项目的所有包中的所有类访问

当编写一个类时,应该把其中的所有属性设置成私有,然后为每个属性设置属性为 public 的 set 和 get 方法,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class person{
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

set get 方法可利用 eclipse 右键菜单内相应选项直接生成。类里面的普通方法一般使用 public 即可。

多态

多态指同一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个方法,具体实现会完全不同。

多态的要点:

1.多态是方法的多态,不是属性的多态(多态与属性无关)

2.多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象。

3.父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了

比如说有 animal , dog ,cat 三个类,其中 dog 和 cat 是 animal 的子类,当定义一个方法,其中的形参是 animal a ,这时传入 dog 类和 cat 类都行,如果不用父类来定义形参,那么就得根据可能出现调用情况分别编写几个形参不同的方法,这就很繁琐

final关键字

final 关键字的作用:

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

2.修饰方法:该方法不可被子类重写,但是可以被重载

3.修饰类:修饰的类不能被继承

foreach 循环

用于读取数组元素的值,不能修改元素的值

其用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test{
public static void main(String[] args){
int[] a = new int[4];
//赋值
for(int i=0;i<a.length;i++)
a[i] = i*100;
//foreach循环遍历
for(int m:a){
System.out.println(m);
}
}

}

抽象类

1.抽象类以及抽象方法使用 abstract 关键词修饰

2.抽象方法指的是只定义了名称但没有内容的方法

3.有抽象方法的类只能定义成抽象类

4.抽象类不能实例化,既不能用 new 来实例化抽象类

5.抽象类可以包含属性、方法、构造方法。但是构造方法不能用来 new 实例,只能用来被子类调用

6.抽象类只能用来继承

7.抽象方法必须被子类实现

抽象方法的意义在于:将方法的设计和方法的实现分离了

接口

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

2.借口就是规范,定义的是一组规则,体现了现实世界中 “如果你是……

则必须能……“ 的思想

3.接口的本质是契约,就像我们人间的法律一样。制定好后大家都遵守

4.项目的具体需求是多变的,我们必须以不变应万变才能从容开发,此处的“不变”就是“规范”。因此,我们开发项目往往都是面向接口编程!

接口的写法如下:

1
2
3
4
5
6
7
8
9
public interface myInterface {

String name = "阿Q";
int age = 60;

void say();
int run(int speed);

}

定义接口的关键字是 interface ,接口内定义的变量会自动被转化为常量,也就是说不管你写不写,定义变量的语句前面都会自动加上 public static final 语句,对于方法声明语句则会自动在前面加上 public abstract 语句

下面再补充一下实现接口的类的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyClass implements myInterface {
void say(){
System.out.println("这是"+myInterface.name);
}

@Override
public int run(int speed) {
//...

return 0;
}

}

1.子类通过 implements 来实现接口中的规范

2.接口不能创建实例,但是可用于声明引用型变量

3.一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是public的

4.一个子类可以同时实现多个接口,不同接口之间使用,隔开

5.接口可以多继承

回调函数

回调函数( callback )又称钩子函数( hook ),回调函数的实现是通过参数传递一个类,然后调用这个类的方法,也就是说传入的类不同,所执行的操作也就会不同,为了保证操作的稳定可用,此处可结合抽象类或接口,在形参上将实参所包含的方法进行限制。实际的例子如下:

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
public class TestHook {

public static void test(operation w){
System.out.println("日常操作1");
System.out.println("日常操作2");
w.work();
System.out.println("日常操作3");
System.out.println("日常操作4");

}

public static void main(String[] args) {
test(new operation1()); //实例化operation1并向test方法传递


}

}
abstract class operation{
public abstract void work();
}

class operation1 extends operation{ //实现抽象类operation

public void work() {
System.out.println("正在工作");
}

}

内部类

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

内部类的作用

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

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

内部类的使用场合

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

非静态内部类

1.非静态内部类必须寄存在一个外部类对象里。因此,如果有一个非静态内部类对象。那么一定存在对应的外部类对象。非静态内部类对象单独属于外部类的某个对象

2.非静态内部类可以使用外部类的成员,但是外部类不能直接访问非静态内部类成员

3.非静态内部类不能有静态方法、静态属性、静态初始化块

4.静态成员不能访问非静态成员: 外部类的静态方法、静态代码块不能访问非静态内部块,包括不能使用非静态内部类定义变量、创建实例。

5.成员变量访问要点:

1)内部类里的局部变量:变量名

(2)内部类属性:this.变量名

(3)外部类属性:外部类名.this。变量名

6.内部类的访问:

 (1)外部类中定义内部类:new innerClass()

(2)外部以外的地方使用非静态内部类:

先需要 new 外部类,再通过外部类实例.new 内部类 的形式创建内部类实例

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public class InnerClass {
public static void main(String[] args){
Face f = new Face();
Face.Nose n = f.new Nose();
n.breath();
}
}

class Face{
String type="瓜子脸";

class Nose{
String type = "酒糟鼻";
void breath(){
System.out.println(type);
System.out.println(Face.this.type);
System.out.println("呼吸!");
}
}
}

当内部类和外部类出现重名变量时,直接使用变量名和 this.变量名 都是引用内部类变量,若要引用外部类变量则需要使用 外部类.this.变量名

静态内部类

1.定义方式:

1
2
3
ststic class ClassName{
//类体
}

2.使用要点

(1)当一个静态内部类存在,并不一定存在对应的外部类对象。因此,静态内部类的实例方法并不能直接访问外部类的实例方法

(2)静态内部类看做外部类的一个静态成员 。因此,外部类的方法可以通过:静态内部类.名字 访问静态内部类的静态成员。通过 new 静态内部类() 访问静态内部类的实例

(3)在外部类的外面创建静态内部类:

外部类.内部静态类 变量名 = new 外部类.内部静态类()

具体代码如下:

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
public class InnerClass {
public static void main(String[] args){
Face f = new Face();
Face.Nose n = f.new Nose();
n.breath();
Face.Ear e = new Face.Ear();
e.listen();
}
}

class Face{
String type="瓜子脸";
static String color = "红润";
String shape = "圆";

class Nose{
String type = "酒糟鼻";
void breath(){
System.out.println(type);
System.out.println(Face.this.type);
System.out.println("呼吸!");
}
}
static class Ear{
void listen(){
System.out.println(color);//静态类能直接访问外部类的静态成员
//System.out.println(shape);静态类并不能直接访问外部类的非静态成员

System.out.println("我在听!");
}
}
}

结合源码分析 String 类用法

技术有限,目前仅对基础部分进行尝试解读

变量的定义

String 类中的字符串是通过字符数组实现的,源码内通过语句 private final char value[] 将字符数组定义为一个不可更改的常量,不过这个不可更改是相对于数组的地址来说的,数组的内容仍然是可以改变的,但是 String 类并没有对外暴露 value 数组的赋值接口,所以说 String 类是不可变字符序列也是没毛病的

构造函数

String 类的构造函数有好几种,现在分别进行分析

1.

1
2
3
public String() {
this.value = "".value;
}

这个构造器不接受参数,对应的声明形式为 String 变量名 = new String() ,这个构造器将一个空字符串("") 的 value 数组地址赋给 String 类内部的成员变量 value,当这个 value 的值被确定以后外部将无法对其进行更改,虽然说外部可以进行两个 String 对象的相互赋值,但这本质上只是对实例对象地址的操作,对象内部的内容并没有变。

2.

1
2
3
4
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}

这个构造器就很神奇,传入的参数竟然是另一个 String 类对象。实例化的代码是String a = new String("abc") 还可以这么写

String c = new String(new String(new String(new String(new String(new String("abc")))))) 不看源码都不知道原来还能这么玩。。。

看构造器的代码,它是将传入对象的 value 数组地址交给新生成的对象,也就是说通过该构造方法生成新对象的内容和原对象是相同的,但两个对象的内存地址是不同的,这种方法相当于将原字符串复制了一份

3.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}

这个构造器要求 3 个参数,其中一个是字符数组,另外两个都是整型数字,这个看看大致也能猜出是从字符数组中截取一段形成一个新的字符串。再来看看构造器的内容:

首先是几个界限判断,然后是一个数组按界限复制并赋值给新对象的 value 成员变量。结合起来就是将数组的[offset,offset+count) 内容复制为新字符串变量内容。需要注意的是,这里面的 offset 也就是起始复制位置是按数组中的位置来定的,比如说 test 这个字符串,其中的 t 就应该是第 0 位,而不是第 1 位。

后面几个看不太懂,以后再看

方法

方法挑几个自己能看得懂的,平时也用得到的看看

length 和 isEmpty

先来两个个简单粗暴地

1
2
3
4
5
6
7
8
//返回字符串长度
public int length() {
return value.length;
}
//返回字符串是否为空
public boolean isEmpty() {
return value.length == 0;
}
equals
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

equals 是用来判断两个字符串是否相等的方法,这个方法里显示判断两个对象是否是同一个对象,之后判断与字符串进行比较的变量是否是 String 类或其子类的实例,不过由于 String 类是不可继承的,所以这就是用来判断是不是 String 类了,再下面就是正常的两个数组的比较了

startsWith
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
int to = toffset;
char pa[] = prefix.value;
int po = 0;
int pc = prefix.value.length;
// Note: toffset might be near -1>>>1.
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}

这个方法是用来判断另一个字符串是否是当前字符串的子串。此方法将当前字符串以及待比较字符串的字符数组分别赋给 ta[] 和 pa[],然后从 ta[toffset] 开始其后的字符与 pa[] 中的一致,那么就可以认为待比较字符串是原字符串的子串。startsOf 还有一个重载的方法没有 int 参数,这个方法是判断原字符串是否以指定字符串开头。

endsWith
1
2
3
public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}

该方法在前一个方法的基础上建立的,主要用来判断待比较字符串与原字符串的尾部是否相同。

replace
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
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */

while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}

replace 的作用是将字符串内指定的字符替换为另一个指定的字符,他的时间复杂度为 n,看来是在设计之初就考虑过效率的问题。不过这个字符串的复制干吗要写成两个循环呢,明明一个循环就能搞定的事。感觉可能是为了减少判断,如果需要替换的字符串在相对较后的位置,这样就能减少很多比较操作,提高效率从点滴做起。。

split(源码看不懂)
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
43
44
45
46
47
48
49
50
51
52
53
public String[] split(String regex, int limit) {
/* fastpath if the regex is a
(1)one-char String and this character is not one of the
RegEx's meta characters ".$|()[{^?*+\\", or
(2)two-char String and the first char is the backslash and
the second is not the ascii digit or ascii letter.
*/
char ch = 0;
if (((regex.value.length == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE))
{
int off = 0;
int next = 0;
boolean limited = limit > 0;
ArrayList<String> list = new ArrayList<>();
while ((next = indexOf(ch, off)) != -1) {
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));
off = next + 1;
} else { // last one
//assert (list.size() == limit - 1);
list.add(substring(off, value.length));
off = value.length;
break;
}
}
// If no match was found, return this
if (off == 0)
return new String[]{this};

// Add remaining segment
if (!limited || list.size() < limit)
list.add(substring(off, value.length));

// Construct result
int resultSize = list.size();
if (limit == 0) {
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
return Pattern.compile(regex).split(this, limit);
}

源码看不懂啥意思,不过这个方法用处挺广的,在这里记录一下。此方法可以用来将字符串按照指定字符切割成多个字符子串。用法是 String[] 变量名 = 字符串.split(作为分割线的字符串) 举个例子

1
2
String a = "abcd";
String[] f =a.split("bc");

这时 f[0] == “a”; f[1] == “d”

valueOf

这个方法就不放源码了,因为这个方法有一系列重载的方法,其功能都是将相关的对象转换为字符串。

equalsIgnoreCase(anotherString)

这个方法用来是 equals 的加强版,忽略大小写进行比较。比如说boolean a = "Abc".equalsIgnoreCase("abc") 返回值将是 true

indexOf 和 lastIndexOf

此方法用来返回指定字符在字符串中的位置,不同的是 indexOf 是从左往右找,lastIndexOf 是从右往左找,相同的是二者都只会返回第一个符合条件字符的位置。

示例 int a = "abcde".indexOf("b") 运行后 a 的值是 1

substring

substring 的作用是在原字符串上截取一个子字符串。该方法有两个重载的方法,分别接受一个和两个 int 类型的参数,接受 1 个参数的是从该参数位置截取到字符串尾部,理所当然,接受两个参数的就是截取两个参数之间部分的字符串。用法示例:

1
2
3
4
System.out.println("abcde".substring(1));
输出 bcde
System.out.println("abcde".substring(0,2));
输出 ab
toLowerCase 和 toUpperCase

这两个方法分别用来将字符串转换成大写和小写,用法示例:

1
2
3
4
System.out.println("Abc".toLowerCase());
输出 abc
System.out.println("Abc".toUpperCase());
输出 ABC

一个小知识点

1
2
3
4
5
6
7
8
9
10
String gh = "a";
for (int i = 0;i<1000;i++){
gh = gh+i;
}


String gh = new String("a");
for (int i = 0;i<1000;i++){
gh = gh+i;
}

第一个语句一共创建了 1001 个对象,第二个对象一共创建了 1002 个对象。这种写法非常浪费空间

StringBuilder 和 StringBuffer

StringBuffer是线程安全的,但效率低;StringBuilder 是线程不安全的,但效率高

看一下 StringBuffer 的用法

1
2
3
4
5
6
7
8
public class TestStringBuilder {
public static void main(String[] args) {
StringBuilder sb = new StringBuffer("123");
System.out.println(sb.append("45678912321456789"));
System.out.println(sb.capacity());

}
}

StringBuiler 是一个可变字符序列,其构造器可接受整数和字符串两种类型的数据,不过字符串传进去也会被转化为字符串长度的整型变量,很鸡贼!构造器接收整型参数后将会创建一个整型变量 +16 长度的字符串数组然后再将字符串存进去,同时可使用 append 方法向 StringBuilder 字符串数组中添加字符串,如果数组长度不够其内部会自动延长数组长度,延长规则是创建一个新数组长度在原有基础上 * 2 + 2 ,然后将原有数组内容复制到新数组中,之后重新比较,如果不行就再次延长。

同时对于上面的 for 循环拼接字符串也有了一个更好的处理方法。

1
2
3
4
StringBuilder gh = "a";
for (int i = 0;i<1000;i++){
gh.append(i);
}

这样写新建的对象数量会少很多

再说一个关于 append 的知识点,查看 append 方法的源码可以发现此方法返回的是一个 this 对象,也就是对象本身,所以在使用 append 方法后仍然可以继续使用其他方法。比如说,我们可以一直 append 。

容器

爱笑的代码狗

容器的本质是列表,但相较于列表,容器有一个非常大的优点就是他能自适应的更改自身的程度,其更改长度的原理是在原来长度的基础上新建一个更长的数组来替换原先的数组。除了这点,其余操作和数组并没有态度的区别(个人感觉)

容器中的一些方法

数组复制

System.arraycopy(原数组,原数组复制起点,新数组,新数组赋值起点,复制长度) 这真的是一个很有用的语句,不仅是在数组复制上,在数组的插入时的元素后移上(原数组复制)也是十分有用的

包装类

虽说 java 是面向对象的语言,但是 java 并不是完全面向对象的,他的 8 个基本数据类型并不是对象,为此,Java 中引入了包装类的概念来将基本数据类型装化为类。8 个基本数据类型分别对应 8 个包装类,除了 int 和 char,其他几个基本数据类型的包装类就是将首字母大写,而 int 的包装类是 Integer ,char 的是 Character。

使用 int 数据类型包装类的语法为:Integer a = new Integer(123); 其他数据类型和这个都差不多。如果要将类重新转化为 基本数据类型则需要使用 int b = a.intValue()

自动拆装箱

上述的相互转换还有一种更简单的写法,这也就是现在要说的自动拆装箱。例如:

1
2
3
Integer a = 123;
int b = a;
int c = a + 2;

上述几个语句都是正确的,为了方便开发者使用包装类,java 团队对编译器进行了处理,使包装类能够根据需要自行处理格式,也就是加几个判断,当遇到上述表达式时自动在语句中加上缺少的关键字。

再来看几个语句

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {

public static void main(String[] args) {
Integer a = 1000;

Integer b = 1000;

System.out.println(a==b);
System.out.println(a.equals(b));

}
}

当执行以上代码时,第一个输出语句将会打印 false ,第二个将会打印 true。可如果将上面的数字由 1000 改为 -128 至 127 之间的数字,两个都将会返回 true,这是因为当数字在这个范围内时 java 在运算和处理时会将包装类当成基本数据类型处理,以提高效率。

IO 流的基本操作

IO 流的操作分为读和写两种操作,首先介绍写操作

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
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class Test {

public static void main(String[] args) throws IOException{
String fileName = "C:"+File.separator+"Users"+File.separator+"C"+File.separator+"desktop"+File.separator+"新建文本文档.txt";
//建立连接,后面的 true表示追加模式

OutputStream output = new FileOutputStream(filEName,true);

String str = "\r\n123安徽";
//将字符串转为字节数据

byte[] b = str.getBytes();
//将字节数据写入文件

output.write(b);
output.close();
}
}

接下来再说读操作

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
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class Test {

public static void main(String[] args) throws IOException{
String fileName = "C:"+File.separator+"Users"+File.separator+"C"+File.separator+"desktop"+File.separator+"新建文本文档.txt";
InputStream input = new FileInputStream(fileName);
//数组 b 表示每次读取的数据大小,缓冲数组

byte[] b = new byte[10];
//input.read(b) 语句将会在流中尽量取数组长度的字节缓冲到数组 b 中,如果数组不够长则会分多次进行读取,每次返回数组 b 中的字节个数,如果返回 -1 则说明文件已经结束。read还有一种无参方法,每次读取 1 字节数据,不过这种读取方法是针对英文字符用的,汉字都是两个字符组成的,所以在读取汉字文件时不能使用小于 2 且不能被 2 整除长度的缓冲数组,否则会乱码。
//但是如果文件是中英文混合的话除非缓冲数组设置特别大能够一次读取完文件,不然的话我感觉这肯定会出错的啊,不知道这种问题咋解决的

while(-1!=input.read(b)){
String str = new String(b);
System.out.print(str);

}
System.out.println(input.read());
input.close();

}
}

关于读取文件提速可以使用 BufferedInputStream 类,这个类设置有一个缓冲区,可以将文件缓冲一部分到内存中以供 cpu 快速调用,等到 cpu 读取完成后再进行下一次缓存。我咋感觉这个方法和上面的读取方式没多大区别呢。。。都是存到内存缓冲,不知道这个方法为啥更快。为此特别验证了一下

1
2
3
4
5
6
7
8
9
import java.io.FileInputStream;
import java.util.Date;

public class CopyAndReplace {

public static void main(String[] args) throws Exception {
FileInputStream f = new FileInputStream("C:\\Users\\C\\Desktop\\杂货\\1.zip");

FileInputStream f1 = new FileInputStream("C:\\Users\\C\\Desktop\\杂货\\1.zip");
-----------本文结束感谢您的阅读-----------
0%