4、数组

140 阅读7分钟

数组声明与分配内存

数组是一组相同类型的数据集合

要使用java数组,必须通过声明数组和内存分配2个步骤

int[] arr = null;    声明数组时会在栈内存开辟空间

arr = new int[3];   分配堆内存会在堆内存开辟空间

数组声明,实际上是在栈内存中保存了此数组的名称(也就是保存了堆内存的引用地址)

关键字new是命令编译器根据括号里的长度在堆内存中开辟一块堆内存供数组使用

为啥声明时,给的值是null?

因为数组属于引用类型,null是引用类型的默认值,表示暂时没有任何指向堆内存空间的引用

当分配内存却没有给元素值时,会根据默认值,如上面整型数组,所有元素默认值为0

一个堆内存可以被多个栈内存空间所指向(一个人有多个名字),每个栈都可以修改堆内存中的值内容,例子见下面的数组的引用传递

形象比喻,栈内存相当于人的名字,堆内存相当于人实体

简明声明并分配内存的数组写法:

int[] arr = new int[3];

数组元素使用

数组是一段连续的内存空间,可以通过索引来访问数组中的元素

java数组的索引编号由0开始

数组长度,可以通过arr.length获取,且最大索引号 = arr.length

数组的动态初始化和静态初始化

1)动态初始化

int[] arr = new int[3];

2)静态初始化(列举法)

int[] arr = {1, 2, 3, 4, 5};

求数组元素最大小值

public static void maxInArrays(){
    // 静态初始化数组
    int[] arr3 = {1, 5, 3, 4, 2};

    // 找最大值、最小值
    int max = arr3[0];
    int min = arr3[0];
    for (int i=1; i<arr3.length; i++){
        if(arr3[i]>max){
            max = arr3[i];
        }

        if(arr3[i]<min){
            min = arr3[i];
        }
}

    System.out.println("max: " + max);
    System.out.println("min: " + min);
}

冒泡排序(双重循环)

public static int[] MaoPaoSort(int[] arr){

    // 数组排序(冒泡)
    // 重要的是控制好轮数和比较的次数,以及关系公式:比较次数=元素个数-1-当前轮数
    int temp = 0;
    for(int i=0; i<arr.length-1; i++){// 一共比较i轮,轮数等于元素个数-1
       for(int j=0; j<arr.length-1-i; j++){// 一轮比较j次,每轮少一次,第一轮要比较元素个数-1次
           if(arr[j] < arr[j+1]){// 升序还是降序取决于这里的比较符
              temp = arr[j];
              arr[j] = arr[j+1];
              arr[j+1] = temp;
           }
        }
    }

    System.out.println("降序结果:");
    for(int i = 0; i<arr.length; i++){
        System.out.println(arr[i]);
    }

    return arr;
}

方法的重载

方法的重载

1)方法名称相同,返回类型相同

2)参数的类型,顺序,个数都不同(注意,指的是入参)

最常见的是构造方法的重载

通过不同的传参可以完成不同功能的实现

public static int add(int a, int b){
    return a + b;
}

public static int add(int a, int b, int c){
    return a + b + c;
}

public static int add(int[] arr){
    int totals = 0;
    for(int i=0; i<arr.length; i++){
        totals = totals + arr[i];
    }
    return totals;
}

System.out.println() 也属于重载方法,入参可以是多种类型,输出参数都为 void

方法的递归调用

递归调用是一种特殊的调用,属于方法自己调用自己本身

比如,完成数组元素的加法

// 递归调用(1+...+100)
// 使用递归的2个点,明确的结束条件,不断改变传入的参数
public static int addHundred(int n){
    if(n == 1){// 明确结束条件
        return 1;
    }else {
        return n + addHundred(n-1);
    }
}

尽量避免使用递归,如果调用不当会导致内存溢出

数组的引用传递

方法的输入参数和返回参数为数组时,因为数组是引用数据类型

如果方法对数组堆内存值做了任何修改(比如赋值),修改结果也会保存下来

可以说数组作为入参,实际上是把堆内存的使用权交给了方法,这就是数组的引用传递

public static void changeArrays(int[] arr){
    arr[1] = 2;
}
    
public static void main(String[] args) {
    int[] arr3 = {1, 5, 3, 4, 2};
    System.out.println("更改前:" + arr3[1]);//5
    changeArrays(arr3);
    System.out.println("更改后:" + arr3[1]);//2
}

java.util.Arrays数组类常用方法

1)数组排序(升序排序)

Arrays.sort(arr3);

2)数组转字符串

System.out.println(Arrays.toString(arr3));

数组的复制

// 复制数组
public static int[] copyArrays(int[] arr){
    int[] arrCopy = new int[arr.length];

    System.arraycopy(arr,0,arrCopy,0, arr.length);

    return arrCopy;
}

Java新特性对数组的支持

1)可变参数

方法入参的个数不再是固定的,可以随机传递

public static int add(int[] arr){
    int totals = 0;
    for(int i=0; i<arr.length; i++){
        totals = totals + arr[i];
    }
    return totals;
}

// 这种写法同上面,可变参数的底层就是动态数组
public static int add(int... args){
    int totals = 0;
    for(int i=0; i<args.length; i++){
        totals = totals + i;
    }

    return  totals;
}

还有我们熟悉的main方法,其参数也是可变参数

public static void main(String[] args) {}

2)foreach输出

// foreach输出 idea快捷键arr.for
public static void printArrays(int[] arr){
    for (int x: arr) {
        System.out.println(x);
    }
}

问题

内存泄漏与内存溢出

1)内存泄漏 memory leak

指的是程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏可以忽略

但是内存泄漏堆积后果很严重,会导致内存溢出

2)内存溢出 out of memory(常见)

指的是程序在申请内存时,没有足够的内存空间供其使用,出现out of memory

(比如声明一个integer,却存了long类型的数据,就会产生内存溢出)

java有一种内存自动回收机制GC,所以大家可以放心的申请和使用对象,但是有时候代码逻辑出现问题,导致无法回收

也就是说你不能再使用这些内存,相当于这部分内存泄漏出去了,而内存泄漏最终会导致内存溢出

JVM虚拟机会针对每一个应用分配一定量的内存,当请求量超过这个值时,就会内存溢出

总的来说

内存泄漏,大部分是因为代码不够严谨,但是运行时不能直接看出问题

内存泄漏不会报错,如果不看日志信息是并不知道有泄漏的,如果一直泄漏的话最终会导致内存溢出,程序挂掉

常见的内存溢出是关于图片的请求,如果没有及时释放内存,会导致内存泄漏,最终程序挂掉

常见的内存泄漏写法

1)单例造成的内存泄漏

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

因为单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了

而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏

这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:

  • 传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长 ;

  • 传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context.getApplicationContext();
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

这样不管传入什么Context最终将使用Application的Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏

2)非静态内部类创建静态实例

3)Handle造成的内存泄漏

4)线程造成的内存泄漏

5)资源未关闭造成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用

应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

常见的内存溢出情况:

1)内存中加载的数据量过于庞大,如一次从数据库取出过多数据

2)集合类中有对对象的引用,使用完后未清空,使得JVM不能回收

3)代码中存在死循环或循环产生过多重复的对象实体

4)使用的第三方软件中的BUG

5)启动参数设定的过小

内存泄漏与内存溢出参考链接: blog.csdn.net/zhanggang74…