Não tem nada de “cagada” aí, isso é o funcionamento normal da linguagem, da forma que ela foi definida. Entender como funcionam os tipos é essencial para não cair nessas “armadilhas”.
Conforme descrito aqui, cada tipo do Java tem um tamanho específico. No caso de int, ele usa 32 bits, e o maior valor que ele pode ter é 231-1 (ou seja, 2.147.483.647 - dois bilhões, cento e quarenta e sete milhões, etc). Se você somar 1 a esse valor, ocorre um overflow e ele passa a ser negativo (no caso, ele passa a ser -2147483648, que é o menor valor possível para um int):
int n = 2147483647;
System.out.println(n + 1); // -2147483648
Pense em um odômetro de carro: a maior quilometragem que ele pode indicar é 999999. Se você andar mais um quilômetro, ele volta para o menor valor possível, que é 000000. O mesmo ocorre com int, a diferença é que o menor valor possível é um número negativo. E não adianta tentar aumentar isso, a quantidade de dígitos é fixa (assim como o tamanho de um int é fixo, então um número maior do que a capacidade deste não vai “caber” na quantidade de bits que ele usa).
Se você está trabalhando com números que podem extrapolar os limites de um int, tem que usar um tipo “maior”. No caso, poderia ser um long, que usa 64 bits (portanto, suporta números bem maiores do que um int):
long n = 999999L * 999999L;
System.out.println(n); // 999998000001
Repare que escrevi os números com o prefixo L para “forçar” que sejam long, pois se eu escrever somente 999999, este é interpretado como um int e a conta será feita com int's, e o resultado ficará errado do mesmo jeito. Forçando os números para long, eu obtenho o resultado correto.
Vale lembrar que todos os tipos possuem limites, e no caso do long, o valor máximo é 263-1 (ou seja, 9.223.372.036.854.775.807 - mais de 9 quintilhões). E se alguma conta ultrapassar este limite, também ocorrerá o overflow:
long n = 999999999999999999L * 999999999999999999L;
System.out.println(n); // -7527149226598858751 <-- overflow!
Se precisa de números ainda maiores, aí tem que usar classes como BigInteger. Só que aí fica um pouco mais chato fazer as contas:
BigInteger n = BigInteger.valueOf(999999999999999999L);
n = n.multiply(n);
System.out.println(n); // 999999999999999998000000000000000001
Já no caso do double, a questão é outra. Você está confundindo o número com a representação do número.
Por exemplo, o número 2. Ele é apenas um “conceito”, um valor… numérico, que representa a ideia de uma determinada quantidade (“duas coisas”).
Mas esse mesmo valor pode ser representado de diferentes formas: como o dígito 2, como 2.0 (ou 2,00000), ou como o texto “dois”, “two”, etc. Todas essas formas são diferentes (contém caracteres diferentes), mas todas elas representam o mesmo valor numérico (o mesmo número).
Sendo assim, se você imprimir um double, ele usa o formato especificado por Double.toString, cuja documentação diz:
se o número é menor que 10-3 ou maior que 107, ele é representado em notação científica
Que é justamente o que aconteceu. O resultado de 9999 * 9999 é 99980001, que é maior que 107 e por isso é mostrado como 9.9980001E7 (o “E7” indica que esse número corresponde a 9.9980001 * 107). Se quer mostrar o número de forma diferente, basta formatá-lo:
double n = 9999 * 9999;
System.out.println(n); // 9.9980001E7
System.out.printf("%.0f", n); // 99980001