【Java】入门学习笔记2(输入输出,方法和数组)

1️⃣输入和输出

1.输出到控制台

基本语法

这里有三种基本输出方式:

System.out.println(msg); // 输出一个字符串, 带换行
System.out.print(msg); // 输出一个字符串, 不带换行
System.out.printf(format, msg); // 格式化输出
  • println 输出的内容自带 \n, print 不带 \n
  • printf 的格式化输出方式和 C 语言的 printf 是基本一致的.

不需要刻意去记住,使用的时候再查询也ok

代码示例

System.out.println("hello world");

int x = 10;
System.out.printf("x = %d\n", x)

2.从键盘输入

输入整型、字符串、浮点型

import java.util.Scanner; // 需要导入 util 包

        Scanner sc = new Scanner(System.in);
        System.out.println("请输入你的姓名:");
        //读取字符串
        String name = sc.nextLine();
        System.out.println("请输入你的年龄:");
        //读取整型
        int age = sc.nextInt();
        System.out.println("请输入你的工资:");
        //读取浮点型
        float salary = sc.nextDouble();
        System.out.println("你的信息如下:");
        System.out.println("姓名: "+name+"\n"+"年龄:"+age+"\n"+"工资:"+salary);
        sc.close(); // 注意, 要记得调用关闭方法
        
// 执行结果
请输入你的姓名:
张三
请输入你的年龄:
18
请输入你的工资:
1000
你的信息如下:
姓名: 张三
年龄:18
工资:1000.0

输入字符

先创建一个Scanner对象,调用Scanner对象的next()方法获取控制台输入的字符串,返回的是一个String类型,因为没有nextChar()方法,所以调用StringcharAt(0)方法获取第一个字符,这样一来,我们就输入了一个字符串:

import java.util.Scanner;

Scanner scanner = new Scanner(System.in);
char c = scanner.next().charAt(0);

next() 和 nextLine()的区别

两者都可以从键盘读取输入,但是又有点差别:

next()方法

import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入字符串:");
        System.out.println("输入的字符串为:" + sc.next());
        System.out.println("结束了");
    }
}

输出结果:

nextLine()方法

import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入字符串:");
        System.out.println("输入的字符串为:" + sc.nextLine());
        System.out.println("结束了");
    }
}

输出结果:

从两者的比较中可以清楚的看到区别,next()方法在键盘中读取的时候,空格和回车都可以作为字符串输入结束,而nextLine()不会将空格作为输入结束,而是将其读取保存到字符串中。

使用 Scanner 循环连续读取

hasnext()方法

import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入字符串:");
        while(sc.hasNext()){
            System.out.println("输入的字符串为:" + sc.next());
        }
        System.out.println("结束了");
    }
}

输出结果:

hasNextLine()方法

import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入字符串:");
        while(sc.hasNextLine()){
            System.out.println("输入的字符串为:" + sc.nextLine());
        }
        System.out.println("结束了");
    }
}

输出结果:

从比较中可以看出,两个方法都可以从键盘连续读取,直到使用Ctrl + D退出连续读取。此时循环读取的时候,next()方法依旧将空格看作一个字符串输入完毕,而nextLine()方法则不会。

循环输入的注意事项

通过在网上搜索,获取到这两种方法的区别:
在检查输入流时:

  • hasNext()方法会判断接下来是否有非空字符.如果有,则返回true,否则返回false
  • hasNextLine() 方法会根据行匹配模式去判断接下来是否有一行(包括空行),如果有,则返回true,否则返回false

比如当前我们有如下测试用例:

7 15 9 5

这个测试用例在牛客网上是以文件的形式进行存储的.
而在 linux 系统中文件的结尾会有一个换行符\n,也就是说从System.in输入流中真正读取到的数据流是这样的:

7 15 9 5\n

程序在处理完5之后,输入流中就只剩下一个换行符\n了,在处理完5之后while再去进行循环判断,此时hasNext()方法和hasNextLine()方法去判断得到的结果就产生了差异.

hasNext()方法会认为之后再没有非空字符,会返回一个false
hasNextLine() 方法会认为换行符\n是一个空行,符合行的匹配模式,则会返回一个true,但实际上由于之后再没有数据了,所以会在读取输入流的时候发生异常,从而导致整个运行报错.

建议方案:

采用hasNextXxxx() 的话,后面也要用nextXxxx():

  • 比如前面用hasNextLine(),那么后面要用 nextLine() 来处理输入;
  • 后面用 nextInt() 方法的话,那么前面要使用 hasNextInt()方法去判断.

执行过程

可以这样进行理解:

方法解释:如果此扫描器的输入(缓冲区)中有另一个token(输入的字符串),则返回true。what? 根本没有提到什么时候返回false。其实执行过程是这样的。重点:当执行到hasNext()时,它会先扫描缓冲区中是否有字符,有则返回true,继续扫描。直到扫描为空,这时并不返回false,而是将方法阻塞,等待你输入内容然后继续扫描。

解决方法

除了使用Ctrl + D,还可以使用

带有参数的重载方法,当扫描到的字符与参数值匹配时返回true在这里插入图片描述

import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入字符串:");
        while(!sc.hasNext("#")){//匹配#返回true,然后取非运算。即以#为结束符号
            System.out.println("输入的字符串为:" + sc.next());
        }
        System.out.println("结束了");
    }
}

运行结果:

3.关闭输入的注意事项

如果创建了两个Scanner的对象,在第一个对象的输入使用完毕后,使用close()进行关闭输入后,再用第二个Scanner的对象从键盘读取输入的时候,此时会报错。

这是因为虽然是两个独立的对象,但是用的是同一个输入流,在调用close()实际上相当System.in.close(),对于后面的Scanner对象来说,System.in已经被关闭了。

解决办法:

在所有其他的类中不使用使用close()方法,最后调用close().
不管有几个Scanner的对象,close()只能调用一次.

2️⃣方法的使用

方法就是一个代码片段. 类似于 C 语言中的 “函数”.

方法存在的意义:

  1. 是能够模块化的组织代码(当代码规模比较复杂的时候)。
  2. 该代码片段可被重复使用,并且可以在多个位置调用。
  3. 让代码更好理解更简单,可以让代码更好地实现解耦。
  4. 直接调用现有方法开发, 不必重复造轮子。

1.方法的基本用法

基本语法

 // 方法定义
 public static 方法返回值 方法名称([参数类型 形参 ...]){
     方法体代码;
     [return 返回值];
 }
     
 // 方法调用
     返回值变量 = 方法名称(实参...);

用法示例

实现一个方法实现两个整数相加

class Test {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
		// 方法的调用
        int ret = add(a, b);
        System.out.println("ret = " + ret);
    }
    // 方法的定义
    public static int add(int x, int y) {
        return x + y;
    }
}

// 执行结果
ret = 30

注意事项

  1. public 和 static 两个关键字后面讲解.
  2. 方法定义时, 参数可以没有. 每个参数要指定类型
  3. 方法定义时, 返回值也可以没有, 如果没有返回值, 则返回值类型应写成 void
  4. 方法定义时的参数称为 “形参”, 方法调用时的参数称为 “实参”.
  5. 方法的定义必须在类之中, 代码书写在调用位置的上方或者下方均可.
    在c语言中,函数的实现必须在使用之前,要么在使用前声明,否则编译报错。
  6. 然而Java 中没有 “函数声明” 这样的概念.
    一旦定义了方法就必须写它的实现.

2.方法调用的执行过程

基本规则

  • 定义方法的时候, 不会执行方法的代码. 只有调用的时候才会执行.
  • 当方法被调用的时候, 会将实参赋值给形参.
  • 参数传递完毕后, 就会执行到方法体代码.
  • 当方法执行完毕之后(遇到 return 语句), 就执行完毕, 回到方法调用位置继续往下执行.
  • 一个方法可以被多次调用.

在IDEA中快速创建方法的快捷键:Alt + Enter
比如要实现两个整数相加的方法:
直接写出调用函数,写明函数名和参数以及返回值,再按下快捷键,选择第一项就可以在后面直接创建一个方法。

之后再对方法进行调整和实现方法体即可快速创建方法,十分方便。

3.实参和形参的关系(重要)

代码示例:

class Test {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        swap(a, b);
        System.out.println("a = " + a + " b = " + b);
    }
    public static void swap(int x, int y) {
        int tmp = x;
        x = y;
        y = tmp;
    }
}

// 运行结果
a = 10 b = 20

结果分析
刚才的代码, 没有完成数据的交换.
对于基础类型来说, 形参相当于实参的拷贝.

原因是Java方法的参数传递只有值传递,只是将实参的值复制给形参而已。

变量的地址对于用户全部是不可见的,不能像c语言中将变量的地址当做形参传递。
原因是JVM将地址全部藏起来了。

解决办法:传引用类型参数(用数组可以解决上述问题)

class Test {
    public static void main(String[] args) {
        int[] arr = {10, 20};
        swap(arr);
        System.out.println("a = " + arr[0] + " b = " + arr[1]);
    }
    public static void swap(int[] arr) {
        int tmp = arr[0];
        arr[0] = arr[1];
        arr[1] = tmp;
    }
}

// 运行结果
a = 20 b = 10

4.方法重载

使用重载

如果想要实现两个整数相加,只需要一个方法就可以了,但是如果还要实现两个甚至更多浮点型相加,并且不想改变方法名的话,就需要用到方法重载。
例如:

class Test {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        int ret = add(a, b);
        System.out.println("ret = " + ret);
        double a2 = 10.5;
        double b2 = 20.5;
        double ret2 = add(a2, b2);
        System.out.println("ret2 = " + ret2);
        double a3 = 10.5;
        double b3 = 10.5;
        double c3 = 20.5;
        double ret3 = add(a3, b3, c3);
        System.out.println("ret3 = " + ret3);
    }
    public static int add(int x, int y) {
        return x + y;
    }
    public static double add(double x, double y) {
        return x + y;
    }
    public static double add(double x, double y, double z) {
        return x + y + z;
    }
}

还有控制输出的println()方法,之所以什么都可以往里面仍进行输出,就因为该方法是重载的,能针对各种传递的参数。

重载的规则

针对同一个类:

  • 方法名相同
  • 方法的参数不同(参数个数或者参数类型)
  • 方法的返回值类型不影响重载.
  • 当两个方法名字相同,参数也相同时,但是返回值不相同的时候,不构成重载

5.方法递归

递归在之前c语言的时候就已经写过了,这里不给出具体阐述了,其思想和实现都是差不多的,只是语法上有些差别。
感兴趣的可以看看之前写的关于c语言函数相关的博客,递归的内容在文章后面
传送门🌀已为您打开

递归(基于c)

然后就是一道困扰我很久的一道递归题(很经典),写了一篇很详细的博客去讲解它,感兴趣的可以戳戳看:

汉诺塔(苦口婆心版,是真的很详细了)

3️⃣数组的定义和使用

1.数组的基本用法

创建数组

// 动态初始化
数据类型[] 数组名称 = new 数据类型 [] { 初始化数据 };

// 静态初始化
数据类型[] 数组名称 = { 初始化数据 };

代码示例:

int[] arr = new int[3];//创建一个长度为3的整型数组,未初始化内部元素都是默认值,这里是0

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

int[] arr = {1, 2, 3};//静态数组编译后,会变成上面那种方式,即动态初始化

int[] arr = new int[2]{1,1};//错误的使用方式

其实数组也可以写成
int arr[] = {1, 2, 3};
这样就和 C 语言更相似了. 但是我们还是更推荐写成 int[] arr 的形式. int和 [] 是一个整体.

数组的使用

int[] arr = {1, 2, 3};
// 获取数组长度
System.out.println("length: " + arr.length); // 执行结果: 3

// 访问数组中的元素
System.out.println(arr[1]); // 执行结果: 2
System.out.println(arr[0]); // 执行结果: 1
arr[2] = 100;
System.out.println(arr[2]); // 执行结果: 100

注意事项:

  1. 使用 arr.length 能够获取到数组的长度. . 这个操作为成员访问操作符. 这里不像 c 语言求数组长度要使用sizeof关键字。
  2. 下标访问操作不能超出有效范围 [0, length - 1] , 如果超出有效范围, 会出现下标越界异常
    抛出 java.lang.ArrayIndexOutOfBoundsException 异常,使用数组一定要下标谨防越界.

遍历数组

除了常见的定义变量充当下标使用外,还可以使用for-each遍历数组,能够更方便的完成对数组的遍历.,可以避免循环条件和更新语句写错.

int[] arr = {1, 2, 3};
for (int x : arr) {
	System.out.println(x);
}
// 执行结果
1
2
3

相当于是创建临时变量,变量依次从数组中拿出值,将值复制给变量而已

2.数组作为方法的参数

基本用法

当参数传内置类型时,对形参进行修改,观察其输出值变化

    public static void main(String[] args) {
        int num = 0;
        func(num);
        System.out.println("num = " + num);
    }
    public static void func(int x) {
        x = 10;
        System.out.println("x = " + x);
    }
    
// 执行结果
x = 10
num = 0

修改形参 x 的值, 不影响实参的 num 值,原因是形参和实参是两块内存:

当传入引用类型数组的时候,修改值观察值变化:

    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        func(arr);
        System.out.println("arr[0] = " + arr[0]);
    }
    public static void func(int[] a) {
        a[0] = 10;
        System.out.println("a[0] = " + a[0]);
    }
// 执行结果
a[0] = 10
arr[0] = 10

为什么数组作方法的参数后就能修改呢?

先了解什么是引用?

引用相当于一个 “别名”, 也可以理解成一个指针.
创建一个引用只是相当于创建了一个很小的变量, 这个变量保存了一个整数, 这个整数表示内存中的一个地址.

针对 int[] arr = new int[]{1, 2, 3} 这样的代码, 内存布局如图:

  1. 当我们创建 new int[]{1, 2, 3} 的时候, 相当于创建了一块内存空间保存三个 int
  2. 接下来执行 int[] arr = new int[]{1, 2, 3} 相当于又创建了一个 int[] 变量, 这个变量是一个引用类型, 里面只保存了一个整数(数组的起始内存地址)
  1. 接下来我们进行传参相当于 int[] a = arr , 内存布局如图
  1. 接下来我们修改 a[0] , 此时是根据 0x100 这样的地址找到对应的内存位置, 将值改成 100

此时已经将 0x100 地址的数据改成了 100 . 那么根据实参 arr 来获取数组内容 arr[0] , 本质上也是获取 0x100地址上的数据, 也是 100.

总结: 所谓的 “引用” 本质上只是存了一个地址. Java 将数组设定成引用类型, 这样的话后续进行数组参数传参, 其实只是将数组的地址传入到函数形参中. 这样可以避免对整个数组的拷贝(数组可能比较长, 那么拷贝开销就会很大).

Java中的null

null 在 Java 中表示 “空引用” , 也就是一个无效的引用.

int[] arr = null;
System.out.println(arr[0]);

// 执行结果
Exception in thread "main" java.lang.NullPointerException
	at Test.main(Test.java:6)

null 的作用类似于 C 语言中的 NULL (空指针), 都是表示一个无效的内存位置. 因此不能对这个内存进行任何读写操作. 一旦尝试读写, 就会抛出 NullPointerException.

注意:在 C 语言中,NULL和 0 号地址是有关系的,即(void *) 0 ,通过强转将0号地址规定成空指针。
但是在Java 中并没有约定 null 和 0 号地址的内存有任何关联.

初识 JVM 内存区域划分(重点)

JVM 的内存被划分成了几个区域, 如图所示:

  • 程序计数器 (PC Register): 只是一个很小的空间, 保存下一条执行的指令的地址.
  • 虚拟机栈(JVM Stack): 重点是存储局部变量表(当然也有其他信息). 我们刚才创建的 int[] arr 这样的存储地址的引用就是在这里保存.
  • 本地方法栈(Native Method Stack): 本地方法栈与虚拟机栈的作用类似. 只不过保存的内容是Native方法的局部变量. 在有些版本的 JVM 实现中(例如HotSpot), 本地方法栈和虚拟机栈是一起的.
  • 堆(Heap): JVM所管理的最大内存区域. 使用 new 创建的对象都是在堆上保存 (例如前面的 new int[]{1, 2,3} )
  • 方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. 方法编译出的的字节码就是保存在这个区域.
  • 运行时常量池(Runtime Constant Pool): 是方法区的一部分, 存放字面量(字符串常量)与符号引用. (注意从 JDK1.7 开始, 运行时常量池在堆上).

Native 方法:
JVM 是一个基于 C++ 实现的程序. 在 Java 程序执行过程中, 本质上也需要调用 C++ 提供的一些函数进行和操作系统底层进行一些交互. 因此在 Java 开发中也会调用到一些 C++ 实现的函数.
这里的 Native 方法就是指这些 C++ 实现的, 再由 Java 来调用的函数.

虚拟机:

  • 局部变量和引用保存在栈上, new 出的对象保存在堆上.
  • 堆的空间非常大, 栈的空间比较小.
  • 堆是整个 JVM 共享一个, 而栈每个线程具有一份(一个 Java 程序中可能存在多个栈).

3.数组作为方法的返回值

此处没有什么好讲的,看下代码了解如何使用:

// 写一个方法, 将数组中的每个元素都 * 2
class Test {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        int[] output = transform(arr);
    }
    public static int[] transform(int[] arr) {
        int[] ret = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            ret[i] = arr[i] * 2;
        }
        return ret;
    }
}

4.数组的实用方法

JDK内置了数组的工具包,即java.util.Arrays 包, 其中包含了一些操作数组的常用方法.

数组转字符串

Arrays.toString()方法

import java.util.Arrays

int[] arr = {1,2,3,4,5,6};
String newArr = Arrays.toString(arr);
System.out.println(newArr);

// 执行结果
[1, 2, 3, 4, 5, 6]

数组拷贝

Arrays.copyOfRange()方法

Arrays.copyOf(原数组名称,拷贝后的新数组长度)
//从数组的第一个元素开始拷贝

Arrays.copyOf()方法

Arrays.copyOfRange(原数组名称,原数组起始位置,原数组结束位置)
//拷贝某个范围
//左闭右开

import java.util.Arrays

int[] arr = {1,2,3,4,5,6};
int[] newArr = Arrays.copyOf(arr, arr.length);
System.out.println("newArr: " + Arrays.toString(newArr));

arr[0] = 10;
System.out.println("arr: " + Arrays.toString(arr));
System.out.println("newArr: " + Arrays.toString(newArr));

// 拷贝某个范围.
int[] newArr = Arrays.copyOfRange(arr, 2, 4);
System.out.println("newArr2: " + Arrays.toString(newArr2));

注意事项: 相比于 newArr = arr 这样的赋值, copyOf 是将数组进行了 深拷贝, 即又创建了一个数组对象, 拷贝原有数组中的所有元素到新数组中. 因此, 修改原数组, 不会影响到新数组.

数组排序

Arrays.sort()方法
//默认升序
//JDK默认的双轴快排

public static void main(String[] args) {
    int[] arr = {9, 5, 2, 7};
    Arrays.sort(arr);
    System.out.println(Arrays.toString(arr));
}

数组二分查找

针对有序数组, 可以使用更高效的二分查找.

啥叫有序数组?
有序分为 “升序” 和 “降序”
如 1 2 3 4 , 依次递增即为升序.
如 4 3 2 1 , 依次递减即为降序.

以升序数组为例, 二分查找的思路是先取中间位置的元素, 看要找的值比中间元素大还是小. 如果小, 就去左边找; 否则就去右边找.

public static void main(String[] args) {
    int[] arr = {1,2,3,4,5,6};
    System.out.println(binarySearch(arr, 6));
}
public static int binarySearch(int[] arr, int toFind) {
    int left = 0;
    int right = arr.length - 1;
    while (left <= right) {
        int mid = (left + right) / 2;
        if (toFind < arr[mid]) {
// 去左侧区间找
            right = mid - 1;
        } else if (toFind > arr[mid]) {
            // 去右侧区间找
            left = mid + 1;
        } else {
// 相等, 说明找到了
            return mid;
        }
    }
// 循环结束, 说明没找到
    return -1;
}
    
// 执行结果
5

5.二维数组

二维数组本质上也就是一维数组, 只不过每个元素又是一个一维数组.

基本语法

数据类型[][] 数组名称 = new 数据类型 [行数][列数] { 初始化数据 };

代码示例

int[][] arr = {
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12}
    };
for (int row = 0; row < arr.length; row++) {
        for (int col = 0; col < arr[row].length; col++) {
        System.out.printf("%d\t", arr[row][col]);
        }
        System.out.println("");
        }
// 执行结果
1  2  3  4
5  6  7  8
9 10 11 12

获取二维数组长度

//定义一个整型数组:3行4列
int a[][] = new int[3][4];
//获取行数---3行
int row = a.length;
//获取列数---4列
int col = a[0].length;

在二维数组内部存放的也是一位数组,arr.length获取的是二维数组的长度,即有多少个一维数组;a[0].length获取的是二维数组中一维数组的长度。

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bruin_du

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值