Eu faria assim:
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
class Teste {
public static void main(String... args) {
Scanner scan = new Scanner(System.in);
List<Integer> numbers = new ArrayList<>();
while (true) {
try {
System.out.println("Insira o número:");
numbers.add(Integer.parseInt(scan.nextLine()));
} catch (NumberFormatException e) {
break;
}
}
System.out.println("Estes foram os números digitados:");
for (int i : numbers)
System.out.println(i);
scan.close();
}
}
A idéia é a seguinte:
Eu usei o nextLine() para ler o input do usuário como uma String ao invés de usar o nextInt(), porque nos meus testes deu menos problemas e porque depois eu uso o parseInt() pra converter está String em um numéro inteiro.
O negócio é que se vc passa pro parseInt() uma String que só contém números, ele faz a conversão normalmente, porém, se passar uma String contendo letras, ele lança uma exceção e cai no bloco catch onde eu uso o break pra encerrar o while.
A linha while(true) faz com que o loop seja executado indefinidamente, no caso, até o usuário digitar um letra qualquer.
Eu declarei a variável numberes como uma List ao invés de ArrayList apenas por uma questão de boas práticas, mas vc pode continuar declarando-a como ArrayList sem problemas.
Uma coisa a se ter em mente é que vc não pode usar tipos primitivos como Type Arguments (o tipo usado em < e >). Vc pode umar apenas Tipos Referencia.
Ou seja, sempre que quiser uma lista de inteiros, vc não pode usar List<int> e sim List<Integer>. Uma lista de characteres ficaria assim List<Character> ao invés de List<char>.