1,数组定义及使用
1.1,定义数组
Java语言是典型的静态语言,因此Java数组是静态的,即当数组被初始化之后,该数组所占的内存空间、数组长度都是不可变的。Java程序中的数组必须经过初始化才可使用。所谓初始化,即创建实际的数组对象,也就是在内存中为数组对象分配内存空间,并为每个数组元素指定初始值。
Java中的数组也是一种基本数据类型,它本身是一种引用类型。例如,int[]就是一种数据类型,与int类型、String类型类似,一样可以使用该类型来定义变量,也可以使用该类型进行类型转换等。使用int[]类型来定义变量、进行类型转换时与使用其他普通类型没有任何却别。int[]类型是一种引用类型,创建int[]类型的对象也就是创建数组,需要使用创建数组的语法。
Java语言支持两种语法格式来定义数组:
int[] score; int score[];
对于这两种格式而言,通常推荐使用第一种格式。因为第一种格式不仅具有更好的语意,而且具有更好的可读性。对于int[] score方式,很容易理解这是定义一个变量,其中的变量名是socre,而变量类型是int[]。因此这种方式既容易理解,也符合定义变量的语法。但是第二种int score[]的可读性就差了,看起来好像定义了一个int的变量,而变量名为score[],与真实含义相去甚远。
1.2,一维数组
存储原理:
int[] score = null;
当声明一个整数型数组score时,score可视为数组类型的变量,此时这个变量并没有包含任何内容,编译器仅会在栈内存中分配一块内存给它,用来保存指向数组实体的地址的名称。(null表示暂时没有任何指向的堆内存空间)JDK1.5之后可以不用给数据默认值。
score[] = new int[3];
进行堆内存分配操作,开辟了3个可供保存整数的内存空间,并把此内存空间的参考地址赋值给score变量。一个数组开辟了堆内存空间之后,将在堆内存空间中保存数据,并将堆内存的操作地址给数据名称score。因为数组是引用数据类型,所以数据变量score所保存的并非数组的实体,而是数据堆内存的参考地址。
int[] score = new int[3];
数组操作中,在栈内存中保存的永远是数组的名称,只开辟了栈内存空间的数组是永远无法使用的,必须有指向的堆内存才可以使用,要想开辟新的对内存空间必须使用new关键字,然后只是将此堆内存的使用权交给了对应的栈内存空间,而且一个对内存空间可以同时被多个占内存空间所指向,即一个人可以有多个名字,一个具体的人就相当于堆内存空间,名字就相当于占内存。
静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度。
int[] score = {1,2,3,4,5,6,7,8,9,10};
动态初始化:执行动态初始化时,程序员只需指定数组的长度,即为每个数组元素指定所需的内存空间,系统将负责为这些数组元素分配初始值。一旦数组初始化后,数组元素的内存空间分配即结束,程序只能改变数组元素的值,而无法改变数组的长度。
- 整数类型(byte、short、int和long):则数组的默认值是0。
- 浮点类型(short、double):则数组的默认值是0.0。
- 字符类型(char):则数组的默认值是'\u0000'。
- 布尔类型(boolean):则数组的默认值是false。
- 引用类型(类、接口、数组):则数组的默认值是null。
int[] score = new int[3];
1.3,内存中的数组
数组引用变量只是一个引用,这个引用变量可以指向任何有效的内存,只有当该引用指向有效内存后,才可通过该数组变量来访问数组元素。
public class Main { public static void main(String[] args) { int[] a = {1,2,3}; int[] b = new int[4]; //int[] b; System.out.println(b.length); b = a; System.out.println(b.length); } } ============================================= 4 3
基本数据类型变量的值被存储在栈内存中是完全错误的!!!
数组初始化:在使用Java数组之前必须先初始化数组。
public class Main { public static void main(String[] args) { int[] a = {1,2,3}; int[] b; b = a; System.out.println(Arrays.toString(b)); } } ============================ [1, 2, 3]
但是现在b变量却没有初始化,这不是互相矛盾吗?其实一点都不矛盾。因为我们把数组变量和数组对象搞混了,数组变量只是一个引用变量;而数组对象就是保存在堆内存中的连续内存空间。对数组执行初始化,其实并不是对数组变量执行初始化,而是在堆内存中创建数组对象——也就是为该数组对象分配一块连续的内存空间,这块连续的内存空间的长度就是数组的长度。虽然b变量看似没有经过初始化,但执行b=a;就会让b变量直接指向一个已经存在的数组。
Java程序中的引用变量并不需要经过所谓的初始化操作,需要进行初始化的是引用变量所引用的对象。比如,数组变量不需要进行初始化操作,而数组对象本身需要进行初始化;对象的引用变量也不需要进行初始化,而对象本身才需要初始化。
Java的局部变量必须由程序员提供初始值,因此如果定义了局部变量的数组变量,程序必须对局部的数据变量进行赋值,即使将其赋值为null。
1.4,二维数组
动态初始化
int score[][]; score = new int[4][3];
静态初始化
int score[][] = { { },{ },{ } }
1.5,数组操作工具类:Arrays
Java提供的Arrays类里包含了一些static修饰的方法可以直接操作数组,这个Arrays类里包含了如下几个static修饰的方法:
- int binarySearch(type[] a, type key):使用二分法查询key元素值在a数组中出现的索引;如果a数组不包含key元素值,则返回负数。调用该方法时要求数组已经按升序排列。
- int binarySearch(type[] a, int fromIndex, int toIndex, type key):只搜索a数组中的fromIndex到toIndex索引的元素。
- type[] copyOf(type[] original, int length):这个方法将会把original数组复制成一个数组,其中length是新数组的长度。如果length大于original数组的长度,则新数组前面的元素就是原数组的所有元素,其余元素补充默认值。反之,则丢失原数组后面的元素。
- type[] copyOfRange(type[] original, int from, int to):带范围的复制。
- boolean equals(type[] a, type[] a2):判断数组是否相等。
- void fill(type[] a, type val):该方法将会把a数组的所有元素都赋值为val。
- void fill(type[] a, int fromIndex, int toIndex, type val):将某一区间的元素都赋值为val。
- void sort(type[] a):对a数组进行排序。
- void sort(type[] a, int fromIndex, int toIndex):将某一区间的元素排序。
- String toString(type[] a):该方法将一个数组转换成一个字符串。
2,方法声明及使用
2.1,方法的定义
方法:方法是一段可重复调用的代码段。
方法与函数:方法由传统的传统函数发展而来,方法与函数有显著的不同:在结构化编程于语言里,函数是一等公民,整个软件由一个个函数组成;在面向对象编程语言里,类才是一等公民,整个系统由一个个的类组成。因此在Java语言里,方法不能单独存在,方法必须属于类或对象。
public static 返回值类型 方法名称(类型 参数1,类型 参数2,...){ 程序语句; [return 表达式]; }
命名规范:定义类时,全部单词的首字母必须大写;定义方法时,第一个单词的首字母小写,之后每隔单词的首字母大写。
2.2,方法的重载(覆盖)和重写(覆写)
区别点 | 重载 | 重写 |
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可以修改 | 一定不能修改 |
异常 | 可以修改 | 可以减少或删除,一定不能抛出新的或更广的一次 |
访问权限 | 可以修改 | 一定不能做更严格的限制 |
区别点 | 重载 | 重写 |
定义 | 方法名称相同,参数的类型或个数不同 | 方法名称、参数的类型、返回值类型全部相同 |
对权限没有要求 | 被重写的方法不能拥有更严格的权限 | |
范围 | 发生在一个类中 | 发生在继承类中 |
重载(覆盖):方法名称相同,但是参数的类型和参数的个数不同。
public static void main(String[] args) { int one = add(10,20); int tow = add(10,20,30); float three = add(10.3f,13.3f); System.out.println(one+" "+tow+" "+three); } public static int add(int x,int y){ return x+y; } public static int add(int x,int y,int z){ return x+y+z; } public static float add(float x,float y){ return x+y; }
注意:重载一定只是在参数上的类型或个数不同,不能通过访问权限、返回类型(可以相同也可以不同)、抛出的异常进行重载。
为什么方法的返回值不能用于区分方法重载:对于int f(){}和void f(){}两个方法,如果这样调用int result = f();,系统可以识别是调用int f(){}方法;但Java调用方法时可忽略方法返回值。如果采用如下方法来调用f(); ,就无法判断调用那个方法,Java系统也就不知道调用那个f()。因此,Java里不能使用方法返回值来判断方法重载。
重写(覆写):重写是子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。在 Java 中,不能重写一个 private 或者 static 方法。
- 一个私有方法是一个只能在其定义的类中使用的方法,其他类无法访问它。因为它只在当前类中可见,所以它不能被任何其他类重写。
- 另一方面,静态方法是与类相关联的,而不是与类的实例相关联的。因为它是与类相关联的,所以它可以直接通过类名来调用。由于静态方法是与类相关联的,因此不能重写。
- 虽然不能重写 private 或 static 方法,但可以在子类中创建一个具有相同名称和参数列表的新方法。这个新方法可以使用不同的实现,但不能与父类中的方法相互作用。这被称为方法隐藏,而不是重写。
class Animal{ public void move(){ System.out.println("动物可以移动"); } } class Dog extends Animal{ public void move(){ System.out.println("狗可以跑和走"); } } public class TestDog{ public static void main(String args[]){ Animal a = new Animal(); // Animal 对象 Animal b = new Dog(); // Dog 对象 a.move();// 执行 Animal 类的方法 b.move();//执行 Dog 类的方法 } }
注意:重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如: 父类中使用public定义的方法,子类的访问权限必须是public。
- super.方法():可以让子类访问父类中的方法。
方法覆写时从private变为default不算方法覆写,是不是覆写最终看是否执行本类方法:从程序结果看,现在调用的是父类中的方法,也就是说子类并没有覆写父类中的方法,而是在子类中重新定义了一个新的方法,所以此时方法并没有被覆写。(也可以这么理解:父类中的private只允许父类使用(子类无法继承父类的私有方法),子类即便用super,也无法访问到,对于该方法,子类和父类没有任何关系,自然不存在覆写)(还可以这么理解:覆写发生在继承类中,print无法被继承,自然也就没有覆写)。
class Peroin{ private void print(){ System.out.println("你好"); } public void fun(){ this.print(); } } class Student extends Peroin{ void print(){ System.out.println("你好啊"); } } public class HelloWord { public static void main(String[] args) { new Student().fun(); } } ============================================ 你好
3,引用传递
引用传递:将堆空间的使用权限交给多个栈内存空间。Java里方法的参数传递方式只有一种:值传递。就是将实际参数的副本传入方法执内,而参数本身不会受到任何影响。虽然 Java 中没有引用传递,但是可以通过传递对象引用的方式来实现类似于引用传递的效果。
3.1,基本类型在方法中的传递
public static void main(String[] args) { int score = 1; fun(score); System.out.println(score); } public static void fun(int x){ x += 10; } ================================= 1
3.2,数组的引用传递
如果要向方法中传递一个数组,则方法的接收参数必须是符合其类型的数组。而且数组属于引用数据类型,所以把数组传递进方法之后,如果方法对数组本身做了任何修改,修改结果也将保存下来,不同于基本数据类型。
public static void main(String[] args) { int score[] = {1,2,3}; fun(score); System.out.println(Arrays.toString(score)); } public static void fun(int x[]){ x[0] += 10; } ================================ [11, 2, 3]
3.3,对象内值的引用传递
class Demo{ int age = 30; } public class HelloWord { public static void main(String[] args) { Demo demo = new Demo(); demo.age = 40; System.out.println("方法调用前: " + demo.age); update(demo); System.out.println("方法调用前: " + demo.age); } public static void update(Demo demo2) { demo2.age = 50; } } ===================================================== 方法调用前: 40 方法调用前: 50
3.4,对象引用传递
public class Main { public static void main(String[] args) { String demo = new String("杜马"); System.out.println("方法调用前: "+demo); update(demo); System.out.println("方法调用前: "+demo); } public static void update(String demo){ demo = null; } } =========================================== 方法调用前: 杜马 方法调用前: 杜马
从程序的运行结果发现,虽然传递的是一个String类型的对象,但是结果没有发生改变,因为字符串一旦声明是不可改变的,改变的是内存地址的执行。
public class Main { public static void main(String[] args) { Main main = new Main(); test(main); System.out.print(main); } public static void test(Main main) { main = null; } } ============================================ Main@5594a1b5
3.5,对象+对象引用传递
class Demo{ String name = "燕双嘤"; } public class HelloWord { public static void main(String[] args) { Demo demo = new Demo(); demo.name = "杜马"; System.out.println("方法调用前: " + demo.name); update(demo); System.out.println("方法调用前: " + demo.name); } public static void update(Demo demo2) { demo2.name = "步鹰"; } } ======================================================== 方法调用前: 杜马 方法调用前: 步鹰
4,Java新特性对数组的支持
4.1,可变参数
public static void main(String[] args) { fun(1); fun(1,2); fun(1,2,3); } public static void fun(int...x){ for (int i=0;i<x.length;i++){ System.out.print(x[i]); } System.out.println(); } ======================= 1 12 123
4.2,foreach输出
public static void main(String[] args) { fun(1); fun(1,2); fun(1,2,3); } public static void fun(int...x){ for (int a:x){ System.out.print(a); } System.out.println(); } ======================= 1 12 123
5,成员变量和局部变量
成员变量指的是在类里定义的变量;局部变量是指在方法里定义的变量。
- 成员变量无须显示初始化,只要为一个类定义了类变量或实例变量,系统就会在这个类的准备阶段或创建该类的实例时进行默认初始化,成员变量默认初始化时的赋值规则与数组动态初始化时数组元素的赋值规则完全相同。
- 类变量的作用域比实例变量的作用域更大:实例变量随实例的存在而存在,而类变量则随类的存在而存在。实例可以访问类变量,同一个类的所有实例访问类变量时,实际上访问的是该类本身的同一个变量,访问了同一片内存区。
Java允许通过实例来访问static修饰的成员变量本身就是一个错误,使用static修饰的变量和方法属于这个类本身,而不属于该类的实例,那么就不应该允许使用实例去调用static修饰的成员变量和方法!
局部变量定以后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。这意味着定义局部变量后,系统并未为这个变量分配内存空间,直到等到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中。
与成员变量不同,局部变量不属于任何类或实例,因此它总是保存在其所在的方法的栈内存中。如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应的内存中;如果局部变量是一个引用类型的变量,则这个变量里存放的是地址,通过改地址引用到该变量实际引用的对象或数组。
栈内存中的变量无须系统垃圾回收,往往随方法或代码块的运行结束而结束。因此,局部变量的作用域是从初始化该变量开始,直到该方法或该代码运行完成而结束。因为局部变量只保存基本类型的值或者对象的引用,因此局部变量所占的内存区通常比较小。
即使在程序中使用局部变量,也应该尽可能地缩小局部变量的作用范围,局部变量的作用范围越小,它在内存里停留的时间就越短,程序运行性能就越好。因此,能用代码块局部变量的地方,就坚决使用方法局部变量。
6,Lambda表达式
6.1,Lambda基础
Lambda是Java 8之后提出,支持将代码块作为方法参数,Lambda表达式允许使用更简洁的代码来创建只有一个抽象方法的接口的实例。Lambda表达式的主要作用就是代替匿名内部类的烦琐语法,它由三部分组成:
- 形参列表:形参列表允许省略形参类型。如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略。
- 箭头(->):必须通过英文中划线和大于符号组成。
- 代码块:如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号,那么这条语句就不要用花括号表示语句结束,而它的代码块中仅有一条省略了return语句,Lambda表达式会自动返回这条语句的值。
interface Eatable { void taste(); } interface Flyable { void fly(String weather); } interface Addable { int add(int a, int b); } public class Main { public void eat(Eatable e) { System.out.println(e); e.taste(); } public void drive(Flyable f) { System.out.println("我正在驾驶:" + f); f.fly("碧空如洗的晴日"); } public void test(Addable add) { System.out.println("5 与 3的和为:" + add.add(5, 3)); } public static void main(String[] args) { Main main = new Main(); main.eat(() -> System.out.println("苹果味道不错!")); main.drive(weather -> { System.out.println("今天天气是:" + weather); System.out.println("直升机飞行平稳"); }); main.test((a, b) -> a + b); } }
上面程序没有按照正常的方法传值形式编写,但是可以正常编译、运行,这说明Lambda表达式实际上将会被当作一个“任意类型”的对象。
6.2,Lambda表达式与函数式接口
Lambda表达式的类型,也被称为“目标类型”,Lambda表达式的目标类型必须是“函数式接口”。函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。
如果采用匿名内部类语法来创建函数式接口的实例,则只需要实现一个抽象方法,在这种情况下即可采用Lambda表达式来创建对象,该表达式创建出来的对象的目标类型就是这个函数式接口。
Runnable r = () -> { for (int i = 0; i < 100; i++) { System.out.println(i); } };
Lambda表达式实现的是匿名方法——因此它只能实现特定函数式接口中的唯一方法。这意味着Lambda表达式有如下两个限制:
- Lambda表达式的目标类型必须是明确的函数式接口。
- Lambad表达式只能为函数式接口创建对象。Lambda表达式只能实现一个方法,因此它只能为只有一个抽象方法的接口创建对象。
Object r = () -> { for (int i = 0; i < 100; i++) { System.out.println(i); } };
上面代码就会出现错误,因为不符合第一条规则。